[Python-checkins] gh-104773: PEP 594: Remove cgi and cgitb modules (#104775)

vstinner webhook-mailer at python.org
Wed May 24 05:05:00 EDT 2023


https://github.com/python/cpython/commit/08d592389603500af398d278af4842cff6f22c33
commit: 08d592389603500af398d278af4842cff6f22c33
branch: main
author: Victor Stinner <vstinner at python.org>
committer: vstinner <vstinner at python.org>
date: 2023-05-24T09:04:53Z
summary:

gh-104773: PEP 594: Remove cgi and cgitb modules (#104775)

* Replace "cgi" with "!cgi" in the Sphinx documentation to avoid
  warnings on broken references.
* test_pyclbr no longer tests the cgi module.

files:
A Misc/NEWS.d/next/Library/2023-05-23-01-47-57.gh-issue-104773.I6MQhb.rst
D Doc/library/cgi.rst
D Doc/library/cgitb.rst
D Lib/cgi.py
D Lib/cgitb.py
D Lib/test/test_cgi.py
D Lib/test/test_cgitb.py
M Doc/library/security_warnings.rst
M Doc/library/superseded.rst
M Doc/whatsnew/2.0.rst
M Doc/whatsnew/2.6.rst
M Doc/whatsnew/3.10.rst
M Doc/whatsnew/3.11.rst
M Doc/whatsnew/3.12.rst
M Doc/whatsnew/3.13.rst
M Doc/whatsnew/3.4.rst
M Doc/whatsnew/3.6.rst
M Doc/whatsnew/3.7.rst
M Doc/whatsnew/3.8.rst
M Doc/whatsnew/3.9.rst
M Lib/test/test_pyclbr.py
M Misc/NEWS.d/3.9.0a1.rst
M Python/stdlib_module_names.h
M Tools/wasm/wasm_assets.py

diff --git a/Doc/library/cgi.rst b/Doc/library/cgi.rst
deleted file mode 100644
index 295a601a7bf1..000000000000
--- a/Doc/library/cgi.rst
+++ /dev/null
@@ -1,564 +0,0 @@
-:mod:`cgi` --- Common Gateway Interface support
-===============================================
-
-.. module:: cgi
-   :synopsis: Helpers for running Python scripts via the Common Gateway Interface.
-   :deprecated:
-
-**Source code:** :source:`Lib/cgi.py`
-
-.. index::
-   pair: WWW; server
-   pair: CGI; protocol
-   pair: HTTP; protocol
-   pair: MIME; headers
-   single: URL
-   single: Common Gateway Interface
-
-.. deprecated-removed:: 3.11 3.13
-   The :mod:`cgi` module is deprecated
-   (see :pep:`PEP 594 <594#cgi>` for details and alternatives).
-
-   The :class:`FieldStorage` class can typically be replaced with
-   :func:`urllib.parse.parse_qsl` for ``GET`` and ``HEAD`` requests,
-   and the :mod:`email.message` module or
-   `multipart <https://pypi.org/project/multipart/>`_ for ``POST`` and ``PUT``.
-   Most :ref:`utility functions <functions-in-cgi-module>` have replacements.
-
---------------
-
-Support module for Common Gateway Interface (CGI) scripts.
-
-This module defines a number of utilities for use by CGI scripts written in
-Python.
-
-The global variable ``maxlen`` can be set to an integer indicating the maximum
-size of a POST request. POST requests larger than this size will result in a
-:exc:`ValueError` being raised during parsing. The default value of this
-variable is ``0``, meaning the request size is unlimited.
-
-.. include:: ../includes/wasm-notavail.rst
-
-Introduction
-------------
-
-.. _cgi-intro:
-
-A CGI script is invoked by an HTTP server, usually to process user input
-submitted through an HTML ``<FORM>`` or ``<ISINDEX>`` element.
-
-Most often, CGI scripts live in the server's special :file:`cgi-bin` directory.
-The HTTP server places all sorts of information about the request (such as the
-client's hostname, the requested URL, the query string, and lots of other
-goodies) in the script's shell environment, executes the script, and sends the
-script's output back to the client.
-
-The script's input is connected to the client too, and sometimes the form data
-is read this way; at other times the form data is passed via the "query string"
-part of the URL.  This module is intended to take care of the different cases
-and provide a simpler interface to the Python script.  It also provides a number
-of utilities that help in debugging scripts, and the latest addition is support
-for file uploads from a form (if your browser supports it).
-
-The output of a CGI script should consist of two sections, separated by a blank
-line.  The first section contains a number of headers, telling the client what
-kind of data is following.  Python code to generate a minimal header section
-looks like this::
-
-   print("Content-Type: text/html")    # HTML is following
-   print()                             # blank line, end of headers
-
-The second section is usually HTML, which allows the client software to display
-nicely formatted text with header, in-line images, etc. Here's Python code that
-prints a simple piece of HTML::
-
-   print("<TITLE>CGI script output</TITLE>")
-   print("<H1>This is my first CGI script</H1>")
-   print("Hello, world!")
-
-
-.. _using-the-cgi-module:
-
-Using the cgi module
---------------------
-
-Begin by writing ``import cgi``.
-
-When you write a new script, consider adding these lines::
-
-   import cgitb
-   cgitb.enable()
-
-This activates a special exception handler that will display detailed reports in
-the web browser if any errors occur.  If you'd rather not show the guts of your
-program to users of your script, you can have the reports saved to files
-instead, with code like this::
-
-   import cgitb
-   cgitb.enable(display=0, logdir="/path/to/logdir")
-
-It's very helpful to use this feature during script development. The reports
-produced by :mod:`cgitb` provide information that can save you a lot of time in
-tracking down bugs.  You can always remove the ``cgitb`` line later when you
-have tested your script and are confident that it works correctly.
-
-To get at submitted form data, use the :class:`FieldStorage` class. If the form
-contains non-ASCII characters, use the *encoding* keyword parameter set to the
-value of the encoding defined for the document. It is usually contained in the
-META tag in the HEAD section of the HTML document or by the
-:mailheader:`Content-Type` header.  This reads the form contents from the
-standard input or the environment (depending on the value of various
-environment variables set according to the CGI standard).  Since it may consume
-standard input, it should be instantiated only once.
-
-The :class:`FieldStorage` instance can be indexed like a Python dictionary.
-It allows membership testing with the :keyword:`in` operator, and also supports
-the standard dictionary method :meth:`~dict.keys` and the built-in function
-:func:`len`.  Form fields containing empty strings are ignored and do not appear
-in the dictionary; to keep such values, provide a true value for the optional
-*keep_blank_values* keyword parameter when creating the :class:`FieldStorage`
-instance.
-
-For instance, the following code (which assumes that the
-:mailheader:`Content-Type` header and blank line have already been printed)
-checks that the fields ``name`` and ``addr`` are both set to a non-empty
-string::
-
-   form = cgi.FieldStorage()
-   if "name" not in form or "addr" not in form:
-       print("<H1>Error</H1>")
-       print("Please fill in the name and addr fields.")
-       return
-   print("<p>name:", form["name"].value)
-   print("<p>addr:", form["addr"].value)
-   ...further form processing here...
-
-Here the fields, accessed through ``form[key]``, are themselves instances of
-:class:`FieldStorage` (or :class:`MiniFieldStorage`, depending on the form
-encoding). The :attr:`~FieldStorage.value` attribute of the instance yields
-the string value of the field.  The :meth:`~FieldStorage.getvalue` method
-returns this string value directly; it also accepts an optional second argument
-as a default to return if the requested key is not present.
-
-If the submitted form data contains more than one field with the same name, the
-object retrieved by ``form[key]`` is not a :class:`FieldStorage` or
-:class:`MiniFieldStorage` instance but a list of such instances.  Similarly, in
-this situation, ``form.getvalue(key)`` would return a list of strings. If you
-expect this possibility (when your HTML form contains multiple fields with the
-same name), use the :meth:`~FieldStorage.getlist` method, which always returns
-a list of values (so that you do not need to special-case the single item
-case).  For example, this code concatenates any number of username fields,
-separated by commas::
-
-   value = form.getlist("username")
-   usernames = ",".join(value)
-
-If a field represents an uploaded file, accessing the value via the
-:attr:`~FieldStorage.value` attribute or the :meth:`~FieldStorage.getvalue`
-method reads the entire file in memory as bytes.  This may not be what you
-want.  You can test for an uploaded file by testing either the
-:attr:`~FieldStorage.filename` attribute or the :attr:`~FieldStorage.file`
-attribute.  You can then read the data from the :attr:`!file`
-attribute before it is automatically closed as part of the garbage collection of
-the :class:`FieldStorage` instance
-(the :func:`~io.RawIOBase.read` and :func:`~io.IOBase.readline` methods will
-return bytes)::
-
-   fileitem = form["userfile"]
-   if fileitem.file:
-       # It's an uploaded file; count lines
-       linecount = 0
-       while True:
-           line = fileitem.file.readline()
-           if not line: break
-           linecount = linecount + 1
-
-:class:`FieldStorage` objects also support being used in a :keyword:`with`
-statement, which will automatically close them when done.
-
-If an error is encountered when obtaining the contents of an uploaded file
-(for example, when the user interrupts the form submission by clicking on
-a Back or Cancel button) the :attr:`~FieldStorage.done` attribute of the
-object for the field will be set to the value -1.
-
-The file upload draft standard entertains the possibility of uploading multiple
-files from one field (using a recursive :mimetype:`multipart/\*` encoding).
-When this occurs, the item will be a dictionary-like :class:`FieldStorage` item.
-This can be determined by testing its :attr:`!type` attribute, which should be
-:mimetype:`multipart/form-data` (or perhaps another MIME type matching
-:mimetype:`multipart/\*`).  In this case, it can be iterated over recursively
-just like the top-level form object.
-
-When a form is submitted in the "old" format (as the query string or as a single
-data part of type :mimetype:`application/x-www-form-urlencoded`), the items will
-actually be instances of the class :class:`MiniFieldStorage`.  In this case, the
-:attr:`!list`, :attr:`!file`, and :attr:`filename` attributes are always ``None``.
-
-A form submitted via POST that also has a query string will contain both
-:class:`FieldStorage` and :class:`MiniFieldStorage` items.
-
-.. versionchanged:: 3.4
-   The :attr:`~FieldStorage.file` attribute is automatically closed upon the
-   garbage collection of the creating :class:`FieldStorage` instance.
-
-.. versionchanged:: 3.5
-   Added support for the context management protocol to the
-   :class:`FieldStorage` class.
-
-
-Higher Level Interface
-----------------------
-
-The previous section explains how to read CGI form data using the
-:class:`FieldStorage` class.  This section describes a higher level interface
-which was added to this class to allow one to do it in a more readable and
-intuitive way.  The interface doesn't make the techniques described in previous
-sections obsolete --- they are still useful to process file uploads efficiently,
-for example.
-
-.. XXX: Is this true ?
-
-The interface consists of two simple methods. Using the methods you can process
-form data in a generic way, without the need to worry whether only one or more
-values were posted under one name.
-
-In the previous section, you learned to write following code anytime you
-expected a user to post more than one value under one name::
-
-   item = form.getvalue("item")
-   if isinstance(item, list):
-       # The user is requesting more than one item.
-   else:
-       # The user is requesting only one item.
-
-This situation is common for example when a form contains a group of multiple
-checkboxes with the same name::
-
-   <input type="checkbox" name="item" value="1" />
-   <input type="checkbox" name="item" value="2" />
-
-In most situations, however, there's only one form control with a particular
-name in a form and then you expect and need only one value associated with this
-name.  So you write a script containing for example this code::
-
-   user = form.getvalue("user").upper()
-
-The problem with the code is that you should never expect that a client will
-provide valid input to your scripts.  For example, if a curious user appends
-another ``user=foo`` pair to the query string, then the script would crash,
-because in this situation the ``getvalue("user")`` method call returns a list
-instead of a string.  Calling the :meth:`~str.upper` method on a list is not valid
-(since lists do not have a method of this name) and results in an
-:exc:`AttributeError` exception.
-
-Therefore, the appropriate way to read form data values was to always use the
-code which checks whether the obtained value is a single value or a list of
-values.  That's annoying and leads to less readable scripts.
-
-A more convenient approach is to use the methods :meth:`~FieldStorage.getfirst`
-and :meth:`~FieldStorage.getlist` provided by this higher level interface.
-
-
-.. method:: FieldStorage.getfirst(name, default=None)
-
-   This method always returns only one value associated with form field *name*.
-   The method returns only the first value in case that more values were posted
-   under such name.  Please note that the order in which the values are received
-   may vary from browser to browser and should not be counted on. [#]_  If no such
-   form field or value exists then the method returns the value specified by the
-   optional parameter *default*.  This parameter defaults to ``None`` if not
-   specified.
-
-
-.. method:: FieldStorage.getlist(name)
-
-   This method always returns a list of values associated with form field *name*.
-   The method returns an empty list if no such form field or value exists for
-   *name*.  It returns a list consisting of one item if only one such value exists.
-
-Using these methods you can write nice compact code::
-
-   import cgi
-   form = cgi.FieldStorage()
-   user = form.getfirst("user", "").upper()    # This way it's safe.
-   for item in form.getlist("item"):
-       do_something(item)
-
-
-.. _functions-in-cgi-module:
-
-Functions
----------
-
-These are useful if you want more control, or if you want to employ some of the
-algorithms implemented in this module in other circumstances.
-
-
-.. function:: parse(fp=None, environ=os.environ, keep_blank_values=False, strict_parsing=False, separator="&")
-
-   Parse a query in the environment or from a file (the file defaults to
-   ``sys.stdin``).  The *keep_blank_values*, *strict_parsing* and *separator* parameters are
-   passed to :func:`urllib.parse.parse_qs` unchanged.
-
-   .. deprecated-removed:: 3.11 3.13
-      This function, like the rest of the :mod:`cgi` module, is deprecated.
-      It can be replaced by calling :func:`urllib.parse.parse_qs` directly
-      on the desired query string (except for ``multipart/form-data`` input,
-      which can be handled as described for :func:`parse_multipart`).
-
-
-.. function:: parse_multipart(fp, pdict, encoding="utf-8", errors="replace", separator="&")
-
-   Parse input of type :mimetype:`multipart/form-data` (for  file uploads).
-   Arguments are *fp* for the input file, *pdict* for a dictionary containing
-   other parameters in the :mailheader:`Content-Type` header, and *encoding*,
-   the request encoding.
-
-   Returns a dictionary just like :func:`urllib.parse.parse_qs`: keys are the
-   field names, each value is a list of values for that field. For non-file
-   fields, the value is a list of strings.
-
-   This is easy to use but not much good if you are expecting megabytes to be
-   uploaded --- in that case, use the :class:`FieldStorage` class instead
-   which is much more flexible.
-
-   .. versionchanged:: 3.7
-      Added the *encoding* and *errors* parameters.  For non-file fields, the
-      value is now a list of strings, not bytes.
-
-   .. versionchanged:: 3.10
-      Added the *separator* parameter.
-
-   .. deprecated-removed:: 3.11 3.13
-      This function, like the rest of the :mod:`cgi` module, is deprecated.
-      It can be replaced with the functionality in the :mod:`email` package
-      (e.g. :class:`email.message.EmailMessage`/:class:`email.message.Message`)
-      which implements the same MIME RFCs, or with the
-      `multipart <https://pypi.org/project/multipart/>`__ PyPI project.
-
-
-.. function:: parse_header(string)
-
-   Parse a MIME header (such as :mailheader:`Content-Type`) into a main value and a
-   dictionary of parameters.
-
-   .. deprecated-removed:: 3.11 3.13
-      This function, like the rest of the :mod:`cgi` module, is deprecated.
-      It can be replaced with the functionality in the :mod:`email` package,
-      which implements the same MIME RFCs.
-
-      For example, with :class:`email.message.EmailMessage`::
-
-          from email.message import EmailMessage
-          msg = EmailMessage()
-          msg['content-type'] = 'application/json; charset="utf8"'
-          main, params = msg.get_content_type(), msg['content-type'].params
-
-
-.. function:: test()
-
-   Robust test CGI script, usable as main program. Writes minimal HTTP headers and
-   formats all information provided to the script in HTML format.
-
-
-.. function:: print_environ()
-
-   Format the shell environment in HTML.
-
-
-.. function:: print_form(form)
-
-   Format a form in HTML.
-
-
-.. function:: print_directory()
-
-   Format the current directory in HTML.
-
-
-.. function:: print_environ_usage()
-
-   Print a list of useful (used by CGI) environment variables in HTML.
-
-
-.. _cgi-security:
-
-Caring about security
----------------------
-
-.. index:: pair: CGI; security
-
-There's one important rule: if you invoke an external program (via
-:func:`os.system`, :func:`os.popen` or other functions with similar
-functionality), make very sure you don't pass arbitrary strings received from
-the client to the shell.  This is a well-known security hole whereby clever
-hackers anywhere on the web can exploit a gullible CGI script to invoke
-arbitrary shell commands.  Even parts of the URL or field names cannot be
-trusted, since the request doesn't have to come from your form!
-
-To be on the safe side, if you must pass a string gotten from a form to a shell
-command, you should make sure the string contains only alphanumeric characters,
-dashes, underscores, and periods.
-
-
-Installing your CGI script on a Unix system
--------------------------------------------
-
-Read the documentation for your HTTP server and check with your local system
-administrator to find the directory where CGI scripts should be installed;
-usually this is in a directory :file:`cgi-bin` in the server tree.
-
-Make sure that your script is readable and executable by "others"; the Unix file
-mode should be ``0o755`` octal (use ``chmod 0755 filename``).  Make sure that the
-first line of the script contains ``#!`` starting in column 1 followed by the
-pathname of the Python interpreter, for instance::
-
-   #!/usr/local/bin/python
-
-Make sure the Python interpreter exists and is executable by "others".
-
-Make sure that any files your script needs to read or write are readable or
-writable, respectively, by "others" --- their mode should be ``0o644`` for
-readable and ``0o666`` for writable.  This is because, for security reasons, the
-HTTP server executes your script as user "nobody", without any special
-privileges.  It can only read (write, execute) files that everybody can read
-(write, execute).  The current directory at execution time is also different (it
-is usually the server's cgi-bin directory) and the set of environment variables
-is also different from what you get when you log in.  In particular, don't count
-on the shell's search path for executables (:envvar:`PATH`) or the Python module
-search path (:envvar:`PYTHONPATH`) to be set to anything interesting.
-
-If you need to load modules from a directory which is not on Python's default
-module search path, you can change the path in your script, before importing
-other modules.  For example::
-
-   import sys
-   sys.path.insert(0, "/usr/home/joe/lib/python")
-   sys.path.insert(0, "/usr/local/lib/python")
-
-(This way, the directory inserted last will be searched first!)
-
-Instructions for non-Unix systems will vary; check your HTTP server's
-documentation (it will usually have a section on CGI scripts).
-
-
-Testing your CGI script
------------------------
-
-Unfortunately, a CGI script will generally not run when you try it from the
-command line, and a script that works perfectly from the command line may fail
-mysteriously when run from the server.  There's one reason why you should still
-test your script from the command line: if it contains a syntax error, the
-Python interpreter won't execute it at all, and the HTTP server will most likely
-send a cryptic error to the client.
-
-Assuming your script has no syntax errors, yet it does not work, you have no
-choice but to read the next section.
-
-
-Debugging CGI scripts
----------------------
-
-.. index:: pair: CGI; debugging
-
-First of all, check for trivial installation errors --- reading the section
-above on installing your CGI script carefully can save you a lot of time.  If
-you wonder whether you have understood the installation procedure correctly, try
-installing a copy of this module file (:file:`cgi.py`) as a CGI script.  When
-invoked as a script, the file will dump its environment and the contents of the
-form in HTML format. Give it the right mode etc., and send it a request.  If it's
-installed in the standard :file:`cgi-bin` directory, it should be possible to
-send it a request by entering a URL into your browser of the form:
-
-.. code-block:: none
-
-   http://yourhostname/cgi-bin/cgi.py?name=Joe+Blow&addr=At+Home
-
-If this gives an error of type 404, the server cannot find the script -- perhaps
-you need to install it in a different directory.  If it gives another error,
-there's an installation problem that you should fix before trying to go any
-further.  If you get a nicely formatted listing of the environment and form
-content (in this example, the fields should be listed as "addr" with value "At
-Home" and "name" with value "Joe Blow"), the :file:`cgi.py` script has been
-installed correctly.  If you follow the same procedure for your own script, you
-should now be able to debug it.
-
-The next step could be to call the :mod:`cgi` module's :func:`test` function
-from your script: replace its main code with the single statement ::
-
-   cgi.test()
-
-This should produce the same results as those gotten from installing the
-:file:`cgi.py` file itself.
-
-When an ordinary Python script raises an unhandled exception (for whatever
-reason: of a typo in a module name, a file that can't be opened, etc.), the
-Python interpreter prints a nice traceback and exits.  While the Python
-interpreter will still do this when your CGI script raises an exception, most
-likely the traceback will end up in one of the HTTP server's log files, or be
-discarded altogether.
-
-Fortunately, once you have managed to get your script to execute *some* code,
-you can easily send tracebacks to the web browser using the :mod:`cgitb` module.
-If you haven't done so already, just add the lines::
-
-   import cgitb
-   cgitb.enable()
-
-to the top of your script.  Then try running it again; when a problem occurs,
-you should see a detailed report that will likely make apparent the cause of the
-crash.
-
-If you suspect that there may be a problem in importing the :mod:`cgitb` module,
-you can use an even more robust approach (which only uses built-in modules)::
-
-   import sys
-   sys.stderr = sys.stdout
-   print("Content-Type: text/plain")
-   print()
-   ...your code here...
-
-This relies on the Python interpreter to print the traceback.  The content type
-of the output is set to plain text, which disables all HTML processing.  If your
-script works, the raw HTML will be displayed by your client.  If it raises an
-exception, most likely after the first two lines have been printed, a traceback
-will be displayed. Because no HTML interpretation is going on, the traceback
-will be readable.
-
-
-Common problems and solutions
------------------------------
-
-* Most HTTP servers buffer the output from CGI scripts until the script is
-  completed.  This means that it is not possible to display a progress report on
-  the client's display while the script is running.
-
-* Check the installation instructions above.
-
-* Check the HTTP server's log files.  (``tail -f logfile`` in a separate window
-  may be useful!)
-
-* Always check a script for syntax errors first, by doing something like
-  ``python script.py``.
-
-* If your script does not have any syntax errors, try adding ``import cgitb;
-  cgitb.enable()`` to the top of the script.
-
-* When invoking external programs, make sure they can be found. Usually, this
-  means using absolute path names --- :envvar:`PATH` is usually not set to a very
-  useful value in a CGI script.
-
-* When reading or writing external files, make sure they can be read or written
-  by the userid under which your CGI script will be running: this is typically the
-  userid under which the web server is running, or some explicitly specified
-  userid for a web server's ``suexec`` feature.
-
-* Don't try to give a CGI script a set-uid mode.  This doesn't work on most
-  systems, and is a security liability as well.
-
-.. rubric:: Footnotes
-
-.. [#] Note that some recent versions of the HTML specification do state what
-   order the field values should be supplied in, but knowing whether a request
-   was received from a conforming browser, or even from a browser at all, is
-   tedious and error-prone.
diff --git a/Doc/library/cgitb.rst b/Doc/library/cgitb.rst
deleted file mode 100644
index 7f00bcd55c1e..000000000000
--- a/Doc/library/cgitb.rst
+++ /dev/null
@@ -1,89 +0,0 @@
-:mod:`cgitb` --- Traceback manager for CGI scripts
-==================================================
-
-.. module:: cgitb
-   :synopsis: Configurable traceback handler for CGI scripts.
-   :deprecated:
-
-.. moduleauthor:: Ka-Ping Yee <ping at lfw.org>
-.. sectionauthor:: Fred L. Drake, Jr. <fdrake at acm.org>
-
-**Source code:** :source:`Lib/cgitb.py`
-
-.. index::
-   single: CGI; exceptions
-   single: CGI; tracebacks
-   single: exceptions; in CGI scripts
-   single: tracebacks; in CGI scripts
-
-.. deprecated-removed:: 3.11 3.13
-   The :mod:`cgitb` module is deprecated
-   (see :pep:`PEP 594 <594#cgitb>` for details).
-
---------------
-
-The :mod:`cgitb` module provides a special exception handler for Python scripts.
-(Its name is a bit misleading.  It was originally designed to display extensive
-traceback information in HTML for CGI scripts.  It was later generalized to also
-display this information in plain text.)  After this module is activated, if an
-uncaught exception occurs, a detailed, formatted report will be displayed.  The
-report includes a traceback showing excerpts of the source code for each level,
-as well as the values of the arguments and local variables to currently running
-functions, to help you debug the problem.  Optionally, you can save this
-information to a file instead of sending it to the browser.
-
-To enable this feature, simply add this to the top of your CGI script::
-
-   import cgitb
-   cgitb.enable()
-
-The options to the :func:`enable` function control whether the report is
-displayed in the browser and whether the report is logged to a file for later
-analysis.
-
-
-.. function:: enable(display=1, logdir=None, context=5, format="html")
-
-   .. index:: single: excepthook() (in module sys)
-
-   This function causes the :mod:`cgitb` module to take over the interpreter's
-   default handling for exceptions by setting the value of :attr:`sys.excepthook`.
-
-   The optional argument *display* defaults to ``1`` and can be set to ``0`` to
-   suppress sending the traceback to the browser. If the argument *logdir* is
-   present, the traceback reports are written to files.  The value of *logdir*
-   should be a directory where these files will be placed. The optional argument
-   *context* is the number of lines of context to display around the current line
-   of source code in the traceback; this defaults to ``5``. If the optional
-   argument *format* is ``"html"``, the output is formatted as HTML.  Any other
-   value forces plain text output.  The default value is ``"html"``.
-
-
-.. function:: text(info, context=5)
-
-   This function handles the exception described by *info* (a 3-tuple containing
-   the result of :func:`sys.exc_info`), formatting its traceback as text and
-   returning the result as a string. The optional argument *context* is the
-   number of lines of context to display around the current line of source code
-   in the traceback; this defaults to ``5``.
-
-
-.. function:: html(info, context=5)
-
-   This function handles the exception described by *info* (a 3-tuple containing
-   the result of :func:`sys.exc_info`), formatting its traceback as HTML and
-   returning the result as a string. The optional argument *context* is the
-   number of lines of context to display around the current line of source code
-   in the traceback; this defaults to ``5``.
-
-
-.. function:: handler(info=None)
-
-   This function handles an exception using the default settings (that is, show a
-   report in the browser, but don't log to a file). This can be used when you've
-   caught an exception and want to report it using :mod:`cgitb`.  The optional
-   *info* argument should be a 3-tuple containing an exception type, exception
-   value, and traceback object, exactly like the tuple returned by
-   :func:`sys.exc_info`.  If the *info* argument is not supplied, the current
-   exception is obtained from :func:`sys.exc_info`.
-
diff --git a/Doc/library/security_warnings.rst b/Doc/library/security_warnings.rst
index 284f36583206..a573c98f73eb 100644
--- a/Doc/library/security_warnings.rst
+++ b/Doc/library/security_warnings.rst
@@ -9,7 +9,6 @@ The following modules have specific security considerations:
 
 * :mod:`base64`: :ref:`base64 security considerations <base64-security>` in
   :rfc:`4648`
-* :mod:`cgi`: :ref:`CGI security considerations <cgi-security>`
 * :mod:`hashlib`: :ref:`all constructors take a "usedforsecurity" keyword-only
   argument disabling known insecure and blocked algorithms
   <hashlib-usedforsecurity>`
diff --git a/Doc/library/superseded.rst b/Doc/library/superseded.rst
index e4a473f71284..a96d042f8374 100644
--- a/Doc/library/superseded.rst
+++ b/Doc/library/superseded.rst
@@ -12,8 +12,6 @@ backwards compatibility. They have been superseded by other modules.
 
    aifc.rst
    audioop.rst
-   cgi.rst
-   cgitb.rst
    chunk.rst
    crypt.rst
    imghdr.rst
diff --git a/Doc/whatsnew/2.0.rst b/Doc/whatsnew/2.0.rst
index 0eefefd863a6..76094d4df98f 100644
--- a/Doc/whatsnew/2.0.rst
+++ b/Doc/whatsnew/2.0.rst
@@ -1030,7 +1030,7 @@ Module changes
 
 Lots of improvements and bugfixes were made to Python's extensive standard
 library; some of the affected modules include :mod:`readline`,
-:mod:`ConfigParser`, :mod:`cgi`, :mod:`calendar`, :mod:`posix`, :mod:`readline`,
+:mod:`ConfigParser`, :mod:`!cgi`, :mod:`calendar`, :mod:`posix`, :mod:`readline`,
 :mod:`xmllib`, :mod:`aifc`, :mod:`chunk, wave`, :mod:`random`, :mod:`shelve`,
 and :mod:`nntplib`.  Consult the CVS logs for the exact patch-by-patch details.
 
diff --git a/Doc/whatsnew/2.6.rst b/Doc/whatsnew/2.6.rst
index 7236e1cad7c8..0a8bad4e5317 100644
--- a/Doc/whatsnew/2.6.rst
+++ b/Doc/whatsnew/2.6.rst
@@ -1805,15 +1805,15 @@ changes, or look through the Subversion logs for all the details.
   available, instead of restricting itself to protocol 1.
   (Contributed by W. Barnes.)
 
-* The :mod:`cgi` module will now read variables from the query string
+* The :mod:`!cgi` module will now read variables from the query string
   of an HTTP POST request.  This makes it possible to use form actions
   with URLs that include query strings such as
   "/cgi-bin/add.py?category=1".  (Contributed by Alexandre Fiori and
   Nubis; :issue:`1817`.)
 
   The :func:`parse_qs` and :func:`parse_qsl` functions have been
-  relocated from the :mod:`cgi` module to the :mod:`urlparse` module.
-  The versions still available in the :mod:`cgi` module will
+  relocated from the :mod:`!cgi` module to the :mod:`urlparse` module.
+  The versions still available in the :mod:`!cgi` module will
   trigger :exc:`PendingDeprecationWarning` messages in 2.6
   (:issue:`600362`).
 
diff --git a/Doc/whatsnew/3.10.rst b/Doc/whatsnew/3.10.rst
index 661eeaedbfc0..ab030db5b3ff 100644
--- a/Doc/whatsnew/3.10.rst
+++ b/Doc/whatsnew/3.10.rst
@@ -1508,7 +1508,7 @@ query parameter separators in :func:`urllib.parse.parse_qs` and
 :func:`urllib.parse.parse_qsl`.  Due to security concerns, and to conform with
 newer W3C recommendations, this has been changed to allow only a single
 separator key, with ``&`` as the default.  This change also affects
-:func:`cgi.parse` and :func:`cgi.parse_multipart` as they use the affected
+:func:`!cgi.parse` and :func:`!cgi.parse_multipart` as they use the affected
 functions internally.  For more details, please see their respective
 documentation.
 (Contributed by Adam Goldschmidt, Senthil Kumaran and Ken Jin in :issue:`42967`.)
diff --git a/Doc/whatsnew/3.11.rst b/Doc/whatsnew/3.11.rst
index 8aadc2a0a581..d024b85059a8 100644
--- a/Doc/whatsnew/3.11.rst
+++ b/Doc/whatsnew/3.11.rst
@@ -1735,9 +1735,9 @@ Modules
   +---------------------+---------------------+---------------------+---------------------+---------------------+
   | :mod:`audioop`      | :mod:`crypt`        | :mod:`nis`          | :mod:`sndhdr`       | :mod:`uu`           |
   +---------------------+---------------------+---------------------+---------------------+---------------------+
-  | :mod:`cgi`          | :mod:`imghdr`       | :mod:`nntplib`      | :mod:`spwd`         | :mod:`xdrlib`       |
+  | :mod:`!cgi`         | :mod:`imghdr`       | :mod:`nntplib`      | :mod:`spwd`         | :mod:`xdrlib`       |
   +---------------------+---------------------+---------------------+---------------------+---------------------+
-  | :mod:`cgitb`        | :mod:`mailcap`      | :mod:`ossaudiodev`  | :mod:`sunau`        |                     |
+  | :mod:`!cgitb`       | :mod:`mailcap`      | :mod:`ossaudiodev`  | :mod:`sunau`        |                     |
   +---------------------+---------------------+---------------------+---------------------+---------------------+
 
   (Contributed by Brett Cannon in :issue:`47061` and Victor Stinner in
diff --git a/Doc/whatsnew/3.12.rst b/Doc/whatsnew/3.12.rst
index 5e07a4caeb9e..417da22ee045 100644
--- a/Doc/whatsnew/3.12.rst
+++ b/Doc/whatsnew/3.12.rst
@@ -805,8 +805,8 @@ Modules (see :pep:`594`):
 
 * :mod:`aifc`
 * :mod:`audioop`
-* :mod:`cgi`
-* :mod:`cgitb`
+* :mod:`!cgi`
+* :mod:`!cgitb`
 * :mod:`chunk`
 * :mod:`crypt`
 * :mod:`imghdr`
diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst
index 309f26ea27db..4bd9a73e7aa7 100644
--- a/Doc/whatsnew/3.13.rst
+++ b/Doc/whatsnew/3.13.rst
@@ -118,6 +118,36 @@ Removed
 * Remove support for using :class:`pathlib.Path` objects as context managers.
   This functionality was deprecated and made a no-op in Python 3.9.
 
+* :pep:`594`: Remove the :mod:`!cgi`` and :mod:`!cgitb` modules,
+  deprecated in Python 3.11.
+
+  * ``cgi.FieldStorage`` can typically be replaced with
+    :func:`urllib.parse.parse_qsl` for ``GET`` and ``HEAD`` requests, and the
+    :mod:`email.message` module or `multipart
+    <https://pypi.org/project/multipart/>`__ PyPI project for ``POST`` and
+    ``PUT``.
+
+  * ``cgi.parse()`` can be replaced by calling :func:`urllib.parse.parse_qs`
+    directly on the desired query string, except for ``multipart/form-data``
+    input, which can be handled as described for ``cgi.parse_multipart()``.
+
+  * ``cgi.parse_multipart()`` can be replaced with the functionality in the
+    :mod:`email` package (e.g. :class:`email.message.EmailMessage` and
+    :class:`email.message.Message`) which implements the same MIME RFCs, or
+    with the `multipart <https://pypi.org/project/multipart/>`__ PyPI project.
+
+  * ``cgi.parse_header()`` can be replaced with the functionality in the
+    :mod:`email` package, which implements the same MIME RFCs. For example,
+    with :class:`email.message.EmailMessage`::
+
+        from email.message import EmailMessage
+        msg = EmailMessage()
+        msg['content-type'] = 'application/json; charset="utf8"'
+        main, params = msg.get_content_type(), msg['content-type'].params
+
+  (Contributed by Victor Stinner in :gh:`104773`.)
+
+
 Porting to Python 3.13
 ======================
 
diff --git a/Doc/whatsnew/3.4.rst b/Doc/whatsnew/3.4.rst
index 45bb91833a35..d4ed8abe772a 100644
--- a/Doc/whatsnew/3.4.rst
+++ b/Doc/whatsnew/3.4.rst
@@ -2371,11 +2371,11 @@ Changes in the Python API
   3.3.3.
 
 * The :attr:`~cgi.FieldStorage.file` attribute is now automatically closed when
-  the creating :class:`cgi.FieldStorage` instance is garbage collected. If you
-  were pulling the file object out separately from the :class:`cgi.FieldStorage`
+  the creating :class:`!cgi.FieldStorage` instance is garbage collected. If you
+  were pulling the file object out separately from the :class:`!cgi.FieldStorage`
   instance and not keeping the instance alive, then you should either store the
-  entire :class:`cgi.FieldStorage` instance or read the contents of the file
-  before the :class:`cgi.FieldStorage` instance is garbage collected.
+  entire :class:`!cgi.FieldStorage` instance or read the contents of the file
+  before the :class:`!cgi.FieldStorage` instance is garbage collected.
 
 * Calling ``read`` or ``write`` on a closed SSL socket now raises an
   informative :exc:`ValueError` rather than the previous more mysterious
diff --git a/Doc/whatsnew/3.6.rst b/Doc/whatsnew/3.6.rst
index 1cd33dfb6042..3d8f9322f927 100644
--- a/Doc/whatsnew/3.6.rst
+++ b/Doc/whatsnew/3.6.rst
@@ -2185,7 +2185,7 @@ Changes in the Python API
 
 * The following modules have had missing APIs added to their :attr:`__all__`
   attributes to match the documented APIs:
-  :mod:`calendar`, :mod:`cgi`, :mod:`csv`,
+  :mod:`calendar`, :mod:`!cgi`, :mod:`csv`,
   :mod:`~xml.etree.ElementTree`, :mod:`enum`,
   :mod:`fileinput`, :mod:`ftplib`, :mod:`logging`, :mod:`mailbox`,
   :mod:`mimetypes`, :mod:`optparse`, :mod:`plistlib`, :mod:`smtpd`,
@@ -2455,7 +2455,7 @@ query parameter separators in :func:`urllib.parse.parse_qs` and
 :func:`urllib.parse.parse_qsl`.  Due to security concerns, and to conform with
 newer W3C recommendations, this has been changed to allow only a single
 separator key, with ``&`` as the default.  This change also affects
-:func:`cgi.parse` and :func:`cgi.parse_multipart` as they use the affected
+:func:`!cgi.parse` and :func:`!cgi.parse_multipart` as they use the affected
 functions internally. For more details, please see their respective
 documentation.
 (Contributed by Adam Goldschmidt, Senthil Kumaran and Ken Jin in :issue:`42967`.)
diff --git a/Doc/whatsnew/3.7.rst b/Doc/whatsnew/3.7.rst
index 28f22836d8d0..41d7e08f2b39 100644
--- a/Doc/whatsnew/3.7.rst
+++ b/Doc/whatsnew/3.7.rst
@@ -2567,7 +2567,7 @@ query parameter separators in :func:`urllib.parse.parse_qs` and
 :func:`urllib.parse.parse_qsl`.  Due to security concerns, and to conform with
 newer W3C recommendations, this has been changed to allow only a single
 separator key, with ``&`` as the default.  This change also affects
-:func:`cgi.parse` and :func:`cgi.parse_multipart` as they use the affected
+:func:`!cgi.parse` and :func:`!cgi.parse_multipart` as they use the affected
 functions internally. For more details, please see their respective
 documentation.
 (Contributed by Adam Goldschmidt, Senthil Kumaran and Ken Jin in :issue:`42967`.)
diff --git a/Doc/whatsnew/3.8.rst b/Doc/whatsnew/3.8.rst
index 85e088b64acb..16762817ab82 100644
--- a/Doc/whatsnew/3.8.rst
+++ b/Doc/whatsnew/3.8.rst
@@ -1774,7 +1774,7 @@ The following features and APIs have been removed from Python 3.8:
   to help eliminate confusion as to what Python interpreter the ``pyvenv``
   script is tied to. (Contributed by Brett Cannon in :issue:`25427`.)
 
-* ``parse_qs``, ``parse_qsl``, and ``escape`` are removed from the :mod:`cgi`
+* ``parse_qs``, ``parse_qsl``, and ``escape`` are removed from the :mod:`!cgi`
   module.  They are deprecated in Python 3.2 or older. They should be imported
   from the ``urllib.parse`` and ``html`` modules instead.
 
@@ -2251,7 +2251,7 @@ query parameter separators in :func:`urllib.parse.parse_qs` and
 :func:`urllib.parse.parse_qsl`.  Due to security concerns, and to conform with
 newer W3C recommendations, this has been changed to allow only a single
 separator key, with ``&`` as the default.  This change also affects
-:func:`cgi.parse` and :func:`cgi.parse_multipart` as they use the affected
+:func:`!cgi.parse` and :func:`!cgi.parse_multipart` as they use the affected
 functions internally. For more details, please see their respective
 documentation.
 (Contributed by Adam Goldschmidt, Senthil Kumaran and Ken Jin in :issue:`42967`.)
diff --git a/Doc/whatsnew/3.9.rst b/Doc/whatsnew/3.9.rst
index 532cabdd7a75..54f5662dad90 100644
--- a/Doc/whatsnew/3.9.rst
+++ b/Doc/whatsnew/3.9.rst
@@ -1559,7 +1559,7 @@ query parameter separators in :func:`urllib.parse.parse_qs` and
 :func:`urllib.parse.parse_qsl`.  Due to security concerns, and to conform with
 newer W3C recommendations, this has been changed to allow only a single
 separator key, with ``&`` as the default.  This change also affects
-:func:`cgi.parse` and :func:`cgi.parse_multipart` as they use the affected
+:func:`!cgi.parse` and :func:`!cgi.parse_multipart` as they use the affected
 functions internally. For more details, please see their respective
 documentation.
 (Contributed by Adam Goldschmidt, Senthil Kumaran and Ken Jin in :issue:`42967`.)
diff --git a/Lib/cgi.py b/Lib/cgi.py
deleted file mode 100755
index 8787567be7c0..000000000000
--- a/Lib/cgi.py
+++ /dev/null
@@ -1,1012 +0,0 @@
-#! /usr/local/bin/python
-
-# NOTE: the above "/usr/local/bin/python" is NOT a mistake.  It is
-# intentionally NOT "/usr/bin/env python".  On many systems
-# (e.g. Solaris), /usr/local/bin is not in $PATH as passed to CGI
-# scripts, and /usr/local/bin is the default directory where Python is
-# installed, so /usr/bin/env would be unable to find python.  Granted,
-# binary installations by Linux vendors often install Python in
-# /usr/bin.  So let those vendors patch cgi.py to match their choice
-# of installation.
-
-"""Support module for CGI (Common Gateway Interface) scripts.
-
-This module defines a number of utilities for use by CGI scripts
-written in Python.
-
-The global variable maxlen can be set to an integer indicating the maximum size
-of a POST request. POST requests larger than this size will result in a
-ValueError being raised during parsing. The default value of this variable is 0,
-meaning the request size is unlimited.
-"""
-
-# History
-# -------
-#
-# Michael McLay started this module.  Steve Majewski changed the
-# interface to SvFormContentDict and FormContentDict.  The multipart
-# parsing was inspired by code submitted by Andreas Paepcke.  Guido van
-# Rossum rewrote, reformatted and documented the module and is currently
-# responsible for its maintenance.
-#
-
-__version__ = "2.6"
-
-
-# Imports
-# =======
-
-from io import StringIO, BytesIO, TextIOWrapper
-from collections.abc import Mapping
-import sys
-import os
-import urllib.parse
-from email.parser import FeedParser
-from email.message import Message
-import html
-import locale
-import tempfile
-import warnings
-
-__all__ = ["MiniFieldStorage", "FieldStorage", "parse", "parse_multipart",
-           "parse_header", "test", "print_exception", "print_environ",
-           "print_form", "print_directory", "print_arguments",
-           "print_environ_usage"]
-
-
-warnings._deprecated(__name__, remove=(3,13))
-
-# Logging support
-# ===============
-
-logfile = ""            # Filename to log to, if not empty
-logfp = None            # File object to log to, if not None
-
-def initlog(*allargs):
-    """Write a log message, if there is a log file.
-
-    Even though this function is called initlog(), you should always
-    use log(); log is a variable that is set either to initlog
-    (initially), to dolog (once the log file has been opened), or to
-    nolog (when logging is disabled).
-
-    The first argument is a format string; the remaining arguments (if
-    any) are arguments to the % operator, so e.g.
-        log("%s: %s", "a", "b")
-    will write "a: b" to the log file, followed by a newline.
-
-    If the global logfp is not None, it should be a file object to
-    which log data is written.
-
-    If the global logfp is None, the global logfile may be a string
-    giving a filename to open, in append mode.  This file should be
-    world writable!!!  If the file can't be opened, logging is
-    silently disabled (since there is no safe place where we could
-    send an error message).
-
-    """
-    global log, logfile, logfp
-    warnings.warn("cgi.log() is deprecated as of 3.10. Use logging instead",
-                  DeprecationWarning, stacklevel=2)
-    if logfile and not logfp:
-        try:
-            logfp = open(logfile, "a", encoding="locale")
-        except OSError:
-            pass
-    if not logfp:
-        log = nolog
-    else:
-        log = dolog
-    log(*allargs)
-
-def dolog(fmt, *args):
-    """Write a log message to the log file.  See initlog() for docs."""
-    logfp.write(fmt%args + "\n")
-
-def nolog(*allargs):
-    """Dummy function, assigned to log when logging is disabled."""
-    pass
-
-def closelog():
-    """Close the log file."""
-    global log, logfile, logfp
-    logfile = ''
-    if logfp:
-        logfp.close()
-        logfp = None
-    log = initlog
-
-log = initlog           # The current logging function
-
-
-# Parsing functions
-# =================
-
-# Maximum input we will accept when REQUEST_METHOD is POST
-# 0 ==> unlimited input
-maxlen = 0
-
-def parse(fp=None, environ=os.environ, keep_blank_values=0,
-          strict_parsing=0, separator='&'):
-    """Parse a query in the environment or from a file (default stdin)
-
-        Arguments, all optional:
-
-        fp              : file pointer; default: sys.stdin.buffer
-
-        environ         : environment dictionary; default: os.environ
-
-        keep_blank_values: flag indicating whether blank values in
-            percent-encoded forms should be treated as blank strings.
-            A true value indicates that blanks should be retained as
-            blank strings.  The default false value indicates that
-            blank values are to be ignored and treated as if they were
-            not included.
-
-        strict_parsing: flag indicating what to do with parsing errors.
-            If false (the default), errors are silently ignored.
-            If true, errors raise a ValueError exception.
-
-        separator: str. The symbol to use for separating the query arguments.
-            Defaults to &.
-    """
-    if fp is None:
-        fp = sys.stdin
-
-    # field keys and values (except for files) are returned as strings
-    # an encoding is required to decode the bytes read from self.fp
-    if hasattr(fp,'encoding'):
-        encoding = fp.encoding
-    else:
-        encoding = 'latin-1'
-
-    # fp.read() must return bytes
-    if isinstance(fp, TextIOWrapper):
-        fp = fp.buffer
-
-    if not 'REQUEST_METHOD' in environ:
-        environ['REQUEST_METHOD'] = 'GET'       # For testing stand-alone
-    if environ['REQUEST_METHOD'] == 'POST':
-        ctype, pdict = parse_header(environ['CONTENT_TYPE'])
-        if ctype == 'multipart/form-data':
-            return parse_multipart(fp, pdict, separator=separator)
-        elif ctype == 'application/x-www-form-urlencoded':
-            clength = int(environ['CONTENT_LENGTH'])
-            if maxlen and clength > maxlen:
-                raise ValueError('Maximum content length exceeded')
-            qs = fp.read(clength).decode(encoding)
-        else:
-            qs = ''                     # Unknown content-type
-        if 'QUERY_STRING' in environ:
-            if qs: qs = qs + '&'
-            qs = qs + environ['QUERY_STRING']
-        elif sys.argv[1:]:
-            if qs: qs = qs + '&'
-            qs = qs + sys.argv[1]
-        environ['QUERY_STRING'] = qs    # XXX Shouldn't, really
-    elif 'QUERY_STRING' in environ:
-        qs = environ['QUERY_STRING']
-    else:
-        if sys.argv[1:]:
-            qs = sys.argv[1]
-        else:
-            qs = ""
-        environ['QUERY_STRING'] = qs    # XXX Shouldn't, really
-    return urllib.parse.parse_qs(qs, keep_blank_values, strict_parsing,
-                                 encoding=encoding, separator=separator)
-
-
-def parse_multipart(fp, pdict, encoding="utf-8", errors="replace", separator='&'):
-    """Parse multipart input.
-
-    Arguments:
-    fp   : input file
-    pdict: dictionary containing other parameters of content-type header
-    encoding, errors: request encoding and error handler, passed to
-        FieldStorage
-
-    Returns a dictionary just like parse_qs(): keys are the field names, each
-    value is a list of values for that field. For non-file fields, the value
-    is a list of strings.
-    """
-    # RFC 2046, Section 5.1 : The "multipart" boundary delimiters are always
-    # represented as 7bit US-ASCII.
-    boundary = pdict['boundary'].decode('ascii')
-    ctype = "multipart/form-data; boundary={}".format(boundary)
-    headers = Message()
-    headers.set_type(ctype)
-    try:
-        headers['Content-Length'] = pdict['CONTENT-LENGTH']
-    except KeyError:
-        pass
-    fs = FieldStorage(fp, headers=headers, encoding=encoding, errors=errors,
-        environ={'REQUEST_METHOD': 'POST'}, separator=separator)
-    return {k: fs.getlist(k) for k in fs}
-
-def _parseparam(s):
-    while s[:1] == ';':
-        s = s[1:]
-        end = s.find(';')
-        while end > 0 and (s.count('"', 0, end) - s.count('\\"', 0, end)) % 2:
-            end = s.find(';', end + 1)
-        if end < 0:
-            end = len(s)
-        f = s[:end]
-        yield f.strip()
-        s = s[end:]
-
-def parse_header(line):
-    """Parse a Content-type like header.
-
-    Return the main content-type and a dictionary of options.
-
-    """
-    parts = _parseparam(';' + line)
-    key = parts.__next__()
-    pdict = {}
-    for p in parts:
-        i = p.find('=')
-        if i >= 0:
-            name = p[:i].strip().lower()
-            value = p[i+1:].strip()
-            if len(value) >= 2 and value[0] == value[-1] == '"':
-                value = value[1:-1]
-                value = value.replace('\\\\', '\\').replace('\\"', '"')
-            pdict[name] = value
-    return key, pdict
-
-
-# Classes for field storage
-# =========================
-
-class MiniFieldStorage:
-
-    """Like FieldStorage, for use when no file uploads are possible."""
-
-    # Dummy attributes
-    filename = None
-    list = None
-    type = None
-    file = None
-    type_options = {}
-    disposition = None
-    disposition_options = {}
-    headers = {}
-
-    def __init__(self, name, value):
-        """Constructor from field name and value."""
-        self.name = name
-        self.value = value
-        # self.file = StringIO(value)
-
-    def __repr__(self):
-        """Return printable representation."""
-        return "MiniFieldStorage(%r, %r)" % (self.name, self.value)
-
-
-class FieldStorage:
-
-    """Store a sequence of fields, reading multipart/form-data.
-
-    This class provides naming, typing, files stored on disk, and
-    more.  At the top level, it is accessible like a dictionary, whose
-    keys are the field names.  (Note: None can occur as a field name.)
-    The items are either a Python list (if there's multiple values) or
-    another FieldStorage or MiniFieldStorage object.  If it's a single
-    object, it has the following attributes:
-
-    name: the field name, if specified; otherwise None
-
-    filename: the filename, if specified; otherwise None; this is the
-        client side filename, *not* the file name on which it is
-        stored (that's a temporary file you don't deal with)
-
-    value: the value as a *string*; for file uploads, this
-        transparently reads the file every time you request the value
-        and returns *bytes*
-
-    file: the file(-like) object from which you can read the data *as
-        bytes* ; None if the data is stored a simple string
-
-    type: the content-type, or None if not specified
-
-    type_options: dictionary of options specified on the content-type
-        line
-
-    disposition: content-disposition, or None if not specified
-
-    disposition_options: dictionary of corresponding options
-
-    headers: a dictionary(-like) object (sometimes email.message.Message or a
-        subclass thereof) containing *all* headers
-
-    The class is subclassable, mostly for the purpose of overriding
-    the make_file() method, which is called internally to come up with
-    a file open for reading and writing.  This makes it possible to
-    override the default choice of storing all files in a temporary
-    directory and unlinking them as soon as they have been opened.
-
-    """
-    def __init__(self, fp=None, headers=None, outerboundary=b'',
-                 environ=os.environ, keep_blank_values=0, strict_parsing=0,
-                 limit=None, encoding='utf-8', errors='replace',
-                 max_num_fields=None, separator='&'):
-        """Constructor.  Read multipart/* until last part.
-
-        Arguments, all optional:
-
-        fp              : file pointer; default: sys.stdin.buffer
-            (not used when the request method is GET)
-            Can be :
-            1. a TextIOWrapper object
-            2. an object whose read() and readline() methods return bytes
-
-        headers         : header dictionary-like object; default:
-            taken from environ as per CGI spec
-
-        outerboundary   : terminating multipart boundary
-            (for internal use only)
-
-        environ         : environment dictionary; default: os.environ
-
-        keep_blank_values: flag indicating whether blank values in
-            percent-encoded forms should be treated as blank strings.
-            A true value indicates that blanks should be retained as
-            blank strings.  The default false value indicates that
-            blank values are to be ignored and treated as if they were
-            not included.
-
-        strict_parsing: flag indicating what to do with parsing errors.
-            If false (the default), errors are silently ignored.
-            If true, errors raise a ValueError exception.
-
-        limit : used internally to read parts of multipart/form-data forms,
-            to exit from the reading loop when reached. It is the difference
-            between the form content-length and the number of bytes already
-            read
-
-        encoding, errors : the encoding and error handler used to decode the
-            binary stream to strings. Must be the same as the charset defined
-            for the page sending the form (content-type : meta http-equiv or
-            header)
-
-        max_num_fields: int. If set, then __init__ throws a ValueError
-            if there are more than n fields read by parse_qsl().
-
-        """
-        method = 'GET'
-        self.keep_blank_values = keep_blank_values
-        self.strict_parsing = strict_parsing
-        self.max_num_fields = max_num_fields
-        self.separator = separator
-        if 'REQUEST_METHOD' in environ:
-            method = environ['REQUEST_METHOD'].upper()
-        self.qs_on_post = None
-        if method == 'GET' or method == 'HEAD':
-            if 'QUERY_STRING' in environ:
-                qs = environ['QUERY_STRING']
-            elif sys.argv[1:]:
-                qs = sys.argv[1]
-            else:
-                qs = ""
-            qs = qs.encode(locale.getpreferredencoding(), 'surrogateescape')
-            fp = BytesIO(qs)
-            if headers is None:
-                headers = {'content-type':
-                           "application/x-www-form-urlencoded"}
-        if headers is None:
-            headers = {}
-            if method == 'POST':
-                # Set default content-type for POST to what's traditional
-                headers['content-type'] = "application/x-www-form-urlencoded"
-            if 'CONTENT_TYPE' in environ:
-                headers['content-type'] = environ['CONTENT_TYPE']
-            if 'QUERY_STRING' in environ:
-                self.qs_on_post = environ['QUERY_STRING']
-            if 'CONTENT_LENGTH' in environ:
-                headers['content-length'] = environ['CONTENT_LENGTH']
-        else:
-            if not (isinstance(headers, (Mapping, Message))):
-                raise TypeError("headers must be mapping or an instance of "
-                                "email.message.Message")
-        self.headers = headers
-        if fp is None:
-            self.fp = sys.stdin.buffer
-        # self.fp.read() must return bytes
-        elif isinstance(fp, TextIOWrapper):
-            self.fp = fp.buffer
-        else:
-            if not (hasattr(fp, 'read') and hasattr(fp, 'readline')):
-                raise TypeError("fp must be file pointer")
-            self.fp = fp
-
-        self.encoding = encoding
-        self.errors = errors
-
-        if not isinstance(outerboundary, bytes):
-            raise TypeError('outerboundary must be bytes, not %s'
-                            % type(outerboundary).__name__)
-        self.outerboundary = outerboundary
-
-        self.bytes_read = 0
-        self.limit = limit
-
-        # Process content-disposition header
-        cdisp, pdict = "", {}
-        if 'content-disposition' in self.headers:
-            cdisp, pdict = parse_header(self.headers['content-disposition'])
-        self.disposition = cdisp
-        self.disposition_options = pdict
-        self.name = None
-        if 'name' in pdict:
-            self.name = pdict['name']
-        self.filename = None
-        if 'filename' in pdict:
-            self.filename = pdict['filename']
-        self._binary_file = self.filename is not None
-
-        # Process content-type header
-        #
-        # Honor any existing content-type header.  But if there is no
-        # content-type header, use some sensible defaults.  Assume
-        # outerboundary is "" at the outer level, but something non-false
-        # inside a multi-part.  The default for an inner part is text/plain,
-        # but for an outer part it should be urlencoded.  This should catch
-        # bogus clients which erroneously forget to include a content-type
-        # header.
-        #
-        # See below for what we do if there does exist a content-type header,
-        # but it happens to be something we don't understand.
-        if 'content-type' in self.headers:
-            ctype, pdict = parse_header(self.headers['content-type'])
-        elif self.outerboundary or method != 'POST':
-            ctype, pdict = "text/plain", {}
-        else:
-            ctype, pdict = 'application/x-www-form-urlencoded', {}
-        self.type = ctype
-        self.type_options = pdict
-        if 'boundary' in pdict:
-            self.innerboundary = pdict['boundary'].encode(self.encoding,
-                                                          self.errors)
-        else:
-            self.innerboundary = b""
-
-        clen = -1
-        if 'content-length' in self.headers:
-            try:
-                clen = int(self.headers['content-length'])
-            except ValueError:
-                pass
-            if maxlen and clen > maxlen:
-                raise ValueError('Maximum content length exceeded')
-        self.length = clen
-        if self.limit is None and clen >= 0:
-            self.limit = clen
-
-        self.list = self.file = None
-        self.done = 0
-        if ctype == 'application/x-www-form-urlencoded':
-            self.read_urlencoded()
-        elif ctype[:10] == 'multipart/':
-            self.read_multi(environ, keep_blank_values, strict_parsing)
-        else:
-            self.read_single()
-
-    def __del__(self):
-        try:
-            self.file.close()
-        except AttributeError:
-            pass
-
-    def __enter__(self):
-        return self
-
-    def __exit__(self, *args):
-        self.file.close()
-
-    def __repr__(self):
-        """Return a printable representation."""
-        return "FieldStorage(%r, %r, %r)" % (
-                self.name, self.filename, self.value)
-
-    def __iter__(self):
-        return iter(self.keys())
-
-    def __getattr__(self, name):
-        if name != 'value':
-            raise AttributeError(name)
-        if self.file:
-            self.file.seek(0)
-            value = self.file.read()
-            self.file.seek(0)
-        elif self.list is not None:
-            value = self.list
-        else:
-            value = None
-        return value
-
-    def __getitem__(self, key):
-        """Dictionary style indexing."""
-        if self.list is None:
-            raise TypeError("not indexable")
-        found = []
-        for item in self.list:
-            if item.name == key: found.append(item)
-        if not found:
-            raise KeyError(key)
-        if len(found) == 1:
-            return found[0]
-        else:
-            return found
-
-    def getvalue(self, key, default=None):
-        """Dictionary style get() method, including 'value' lookup."""
-        if key in self:
-            value = self[key]
-            if isinstance(value, list):
-                return [x.value for x in value]
-            else:
-                return value.value
-        else:
-            return default
-
-    def getfirst(self, key, default=None):
-        """ Return the first value received."""
-        if key in self:
-            value = self[key]
-            if isinstance(value, list):
-                return value[0].value
-            else:
-                return value.value
-        else:
-            return default
-
-    def getlist(self, key):
-        """ Return list of received values."""
-        if key in self:
-            value = self[key]
-            if isinstance(value, list):
-                return [x.value for x in value]
-            else:
-                return [value.value]
-        else:
-            return []
-
-    def keys(self):
-        """Dictionary style keys() method."""
-        if self.list is None:
-            raise TypeError("not indexable")
-        return list(set(item.name for item in self.list))
-
-    def __contains__(self, key):
-        """Dictionary style __contains__ method."""
-        if self.list is None:
-            raise TypeError("not indexable")
-        return any(item.name == key for item in self.list)
-
-    def __len__(self):
-        """Dictionary style len(x) support."""
-        return len(self.keys())
-
-    def __bool__(self):
-        if self.list is None:
-            raise TypeError("Cannot be converted to bool.")
-        return bool(self.list)
-
-    def read_urlencoded(self):
-        """Internal: read data in query string format."""
-        qs = self.fp.read(self.length)
-        if not isinstance(qs, bytes):
-            raise ValueError("%s should return bytes, got %s" \
-                             % (self.fp, type(qs).__name__))
-        qs = qs.decode(self.encoding, self.errors)
-        if self.qs_on_post:
-            qs += '&' + self.qs_on_post
-        query = urllib.parse.parse_qsl(
-            qs, self.keep_blank_values, self.strict_parsing,
-            encoding=self.encoding, errors=self.errors,
-            max_num_fields=self.max_num_fields, separator=self.separator)
-        self.list = [MiniFieldStorage(key, value) for key, value in query]
-        self.skip_lines()
-
-    FieldStorageClass = None
-
-    def read_multi(self, environ, keep_blank_values, strict_parsing):
-        """Internal: read a part that is itself multipart."""
-        ib = self.innerboundary
-        if not valid_boundary(ib):
-            raise ValueError('Invalid boundary in multipart form: %r' % (ib,))
-        self.list = []
-        if self.qs_on_post:
-            query = urllib.parse.parse_qsl(
-                self.qs_on_post, self.keep_blank_values, self.strict_parsing,
-                encoding=self.encoding, errors=self.errors,
-                max_num_fields=self.max_num_fields, separator=self.separator)
-            self.list.extend(MiniFieldStorage(key, value) for key, value in query)
-
-        klass = self.FieldStorageClass or self.__class__
-        first_line = self.fp.readline() # bytes
-        if not isinstance(first_line, bytes):
-            raise ValueError("%s should return bytes, got %s" \
-                             % (self.fp, type(first_line).__name__))
-        self.bytes_read += len(first_line)
-
-        # Ensure that we consume the file until we've hit our inner boundary
-        while (first_line.strip() != (b"--" + self.innerboundary) and
-                first_line):
-            first_line = self.fp.readline()
-            self.bytes_read += len(first_line)
-
-        # Propagate max_num_fields into the sub class appropriately
-        max_num_fields = self.max_num_fields
-        if max_num_fields is not None:
-            max_num_fields -= len(self.list)
-
-        while True:
-            parser = FeedParser()
-            hdr_text = b""
-            while True:
-                data = self.fp.readline()
-                hdr_text += data
-                if not data.strip():
-                    break
-            if not hdr_text:
-                break
-            # parser takes strings, not bytes
-            self.bytes_read += len(hdr_text)
-            parser.feed(hdr_text.decode(self.encoding, self.errors))
-            headers = parser.close()
-
-            # Some clients add Content-Length for part headers, ignore them
-            if 'content-length' in headers:
-                del headers['content-length']
-
-            limit = None if self.limit is None \
-                else self.limit - self.bytes_read
-            part = klass(self.fp, headers, ib, environ, keep_blank_values,
-                         strict_parsing, limit,
-                         self.encoding, self.errors, max_num_fields, self.separator)
-
-            if max_num_fields is not None:
-                max_num_fields -= 1
-                if part.list:
-                    max_num_fields -= len(part.list)
-                if max_num_fields < 0:
-                    raise ValueError('Max number of fields exceeded')
-
-            self.bytes_read += part.bytes_read
-            self.list.append(part)
-            if part.done or self.bytes_read >= self.length > 0:
-                break
-        self.skip_lines()
-
-    def read_single(self):
-        """Internal: read an atomic part."""
-        if self.length >= 0:
-            self.read_binary()
-            self.skip_lines()
-        else:
-            self.read_lines()
-        self.file.seek(0)
-
-    bufsize = 8*1024            # I/O buffering size for copy to file
-
-    def read_binary(self):
-        """Internal: read binary data."""
-        self.file = self.make_file()
-        todo = self.length
-        if todo >= 0:
-            while todo > 0:
-                data = self.fp.read(min(todo, self.bufsize)) # bytes
-                if not isinstance(data, bytes):
-                    raise ValueError("%s should return bytes, got %s"
-                                     % (self.fp, type(data).__name__))
-                self.bytes_read += len(data)
-                if not data:
-                    self.done = -1
-                    break
-                self.file.write(data)
-                todo = todo - len(data)
-
-    def read_lines(self):
-        """Internal: read lines until EOF or outerboundary."""
-        if self._binary_file:
-            self.file = self.__file = BytesIO() # store data as bytes for files
-        else:
-            self.file = self.__file = StringIO() # as strings for other fields
-        if self.outerboundary:
-            self.read_lines_to_outerboundary()
-        else:
-            self.read_lines_to_eof()
-
-    def __write(self, line):
-        """line is always bytes, not string"""
-        if self.__file is not None:
-            if self.__file.tell() + len(line) > 1000:
-                self.file = self.make_file()
-                data = self.__file.getvalue()
-                self.file.write(data)
-                self.__file = None
-        if self._binary_file:
-            # keep bytes
-            self.file.write(line)
-        else:
-            # decode to string
-            self.file.write(line.decode(self.encoding, self.errors))
-
-    def read_lines_to_eof(self):
-        """Internal: read lines until EOF."""
-        while 1:
-            line = self.fp.readline(1<<16) # bytes
-            self.bytes_read += len(line)
-            if not line:
-                self.done = -1
-                break
-            self.__write(line)
-
-    def read_lines_to_outerboundary(self):
-        """Internal: read lines until outerboundary.
-        Data is read as bytes: boundaries and line ends must be converted
-        to bytes for comparisons.
-        """
-        next_boundary = b"--" + self.outerboundary
-        last_boundary = next_boundary + b"--"
-        delim = b""
-        last_line_lfend = True
-        _read = 0
-        while 1:
-
-            if self.limit is not None and 0 <= self.limit <= _read:
-                break
-            line = self.fp.readline(1<<16) # bytes
-            self.bytes_read += len(line)
-            _read += len(line)
-            if not line:
-                self.done = -1
-                break
-            if delim == b"\r":
-                line = delim + line
-                delim = b""
-            if line.startswith(b"--") and last_line_lfend:
-                strippedline = line.rstrip()
-                if strippedline == next_boundary:
-                    break
-                if strippedline == last_boundary:
-                    self.done = 1
-                    break
-            odelim = delim
-            if line.endswith(b"\r\n"):
-                delim = b"\r\n"
-                line = line[:-2]
-                last_line_lfend = True
-            elif line.endswith(b"\n"):
-                delim = b"\n"
-                line = line[:-1]
-                last_line_lfend = True
-            elif line.endswith(b"\r"):
-                # We may interrupt \r\n sequences if they span the 2**16
-                # byte boundary
-                delim = b"\r"
-                line = line[:-1]
-                last_line_lfend = False
-            else:
-                delim = b""
-                last_line_lfend = False
-            self.__write(odelim + line)
-
-    def skip_lines(self):
-        """Internal: skip lines until outer boundary if defined."""
-        if not self.outerboundary or self.done:
-            return
-        next_boundary = b"--" + self.outerboundary
-        last_boundary = next_boundary + b"--"
-        last_line_lfend = True
-        while True:
-            line = self.fp.readline(1<<16)
-            self.bytes_read += len(line)
-            if not line:
-                self.done = -1
-                break
-            if line.endswith(b"--") and last_line_lfend:
-                strippedline = line.strip()
-                if strippedline == next_boundary:
-                    break
-                if strippedline == last_boundary:
-                    self.done = 1
-                    break
-            last_line_lfend = line.endswith(b'\n')
-
-    def make_file(self):
-        """Overridable: return a readable & writable file.
-
-        The file will be used as follows:
-        - data is written to it
-        - seek(0)
-        - data is read from it
-
-        The file is opened in binary mode for files, in text mode
-        for other fields
-
-        This version opens a temporary file for reading and writing,
-        and immediately deletes (unlinks) it.  The trick (on Unix!) is
-        that the file can still be used, but it can't be opened by
-        another process, and it will automatically be deleted when it
-        is closed or when the current process terminates.
-
-        If you want a more permanent file, you derive a class which
-        overrides this method.  If you want a visible temporary file
-        that is nevertheless automatically deleted when the script
-        terminates, try defining a __del__ method in a derived class
-        which unlinks the temporary files you have created.
-
-        """
-        if self._binary_file:
-            return tempfile.TemporaryFile("wb+")
-        else:
-            return tempfile.TemporaryFile("w+",
-                encoding=self.encoding, newline = '\n')
-
-
-# Test/debug code
-# ===============
-
-def test(environ=os.environ):
-    """Robust test CGI script, usable as main program.
-
-    Write minimal HTTP headers and dump all information provided to
-    the script in HTML form.
-
-    """
-    print("Content-type: text/html")
-    print()
-    sys.stderr = sys.stdout
-    try:
-        form = FieldStorage()   # Replace with other classes to test those
-        print_directory()
-        print_arguments()
-        print_form(form)
-        print_environ(environ)
-        print_environ_usage()
-        def f():
-            exec("testing print_exception() -- <I>italics?</I>")
-        def g(f=f):
-            f()
-        print("<H3>What follows is a test, not an actual exception:</H3>")
-        g()
-    except:
-        print_exception()
-
-    print("<H1>Second try with a small maxlen...</H1>")
-
-    global maxlen
-    maxlen = 50
-    try:
-        form = FieldStorage()   # Replace with other classes to test those
-        print_directory()
-        print_arguments()
-        print_form(form)
-        print_environ(environ)
-    except:
-        print_exception()
-
-def print_exception(type=None, value=None, tb=None, limit=None):
-    if type is None:
-        type, value, tb = sys.exc_info()
-    import traceback
-    print()
-    print("<H3>Traceback (most recent call last):</H3>")
-    list = traceback.format_tb(tb, limit) + \
-           traceback.format_exception_only(type, value)
-    print("<PRE>%s<B>%s</B></PRE>" % (
-        html.escape("".join(list[:-1])),
-        html.escape(list[-1]),
-        ))
-    del tb
-
-def print_environ(environ=os.environ):
-    """Dump the shell environment as HTML."""
-    keys = sorted(environ.keys())
-    print()
-    print("<H3>Shell Environment:</H3>")
-    print("<DL>")
-    for key in keys:
-        print("<DT>", html.escape(key), "<DD>", html.escape(environ[key]))
-    print("</DL>")
-    print()
-
-def print_form(form):
-    """Dump the contents of a form as HTML."""
-    keys = sorted(form.keys())
-    print()
-    print("<H3>Form Contents:</H3>")
-    if not keys:
-        print("<P>No form fields.")
-    print("<DL>")
-    for key in keys:
-        print("<DT>" + html.escape(key) + ":", end=' ')
-        value = form[key]
-        print("<i>" + html.escape(repr(type(value))) + "</i>")
-        print("<DD>" + html.escape(repr(value)))
-    print("</DL>")
-    print()
-
-def print_directory():
-    """Dump the current directory as HTML."""
-    print()
-    print("<H3>Current Working Directory:</H3>")
-    try:
-        pwd = os.getcwd()
-    except OSError as msg:
-        print("OSError:", html.escape(str(msg)))
-    else:
-        print(html.escape(pwd))
-    print()
-
-def print_arguments():
-    print()
-    print("<H3>Command Line Arguments:</H3>")
-    print()
-    print(sys.argv)
-    print()
-
-def print_environ_usage():
-    """Dump a list of environment variables used by CGI as HTML."""
-    print("""
-<H3>These environment variables could have been set:</H3>
-<UL>
-<LI>AUTH_TYPE
-<LI>CONTENT_LENGTH
-<LI>CONTENT_TYPE
-<LI>DATE_GMT
-<LI>DATE_LOCAL
-<LI>DOCUMENT_NAME
-<LI>DOCUMENT_ROOT
-<LI>DOCUMENT_URI
-<LI>GATEWAY_INTERFACE
-<LI>LAST_MODIFIED
-<LI>PATH
-<LI>PATH_INFO
-<LI>PATH_TRANSLATED
-<LI>QUERY_STRING
-<LI>REMOTE_ADDR
-<LI>REMOTE_HOST
-<LI>REMOTE_IDENT
-<LI>REMOTE_USER
-<LI>REQUEST_METHOD
-<LI>SCRIPT_NAME
-<LI>SERVER_NAME
-<LI>SERVER_PORT
-<LI>SERVER_PROTOCOL
-<LI>SERVER_ROOT
-<LI>SERVER_SOFTWARE
-</UL>
-In addition, HTTP headers sent by the server may be passed in the
-environment as well.  Here are some common variable names:
-<UL>
-<LI>HTTP_ACCEPT
-<LI>HTTP_CONNECTION
-<LI>HTTP_HOST
-<LI>HTTP_PRAGMA
-<LI>HTTP_REFERER
-<LI>HTTP_USER_AGENT
-</UL>
-""")
-
-
-# Utilities
-# =========
-
-def valid_boundary(s):
-    import re
-    if isinstance(s, bytes):
-        _vb_pattern = b"^[ -~]{0,200}[!-~]$"
-    else:
-        _vb_pattern = "^[ -~]{0,200}[!-~]$"
-    return re.match(_vb_pattern, s)
-
-# Invoke mainline
-# ===============
-
-# Call test() when this file is run as a script (not imported as a module)
-if __name__ == '__main__':
-    test()
diff --git a/Lib/cgitb.py b/Lib/cgitb.py
deleted file mode 100644
index f6b97f25c59d..000000000000
--- a/Lib/cgitb.py
+++ /dev/null
@@ -1,332 +0,0 @@
-"""More comprehensive traceback formatting for Python scripts.
-
-To enable this module, do:
-
-    import cgitb; cgitb.enable()
-
-at the top of your script.  The optional arguments to enable() are:
-
-    display     - if true, tracebacks are displayed in the web browser
-    logdir      - if set, tracebacks are written to files in this directory
-    context     - number of lines of source code to show for each stack frame
-    format      - 'text' or 'html' controls the output format
-
-By default, tracebacks are displayed but not saved, the context is 5 lines
-and the output format is 'html' (for backwards compatibility with the
-original use of this module)
-
-Alternatively, if you have caught an exception and want cgitb to display it
-for you, call cgitb.handler().  The optional argument to handler() is a
-3-item tuple (etype, evalue, etb) just like the value of sys.exc_info().
-The default handler displays output as HTML.
-
-"""
-import inspect
-import keyword
-import linecache
-import os
-import pydoc
-import sys
-import tempfile
-import time
-import tokenize
-import traceback
-import warnings
-from html import escape as html_escape
-
-warnings._deprecated(__name__, remove=(3, 13))
-
-
-def reset():
-    """Return a string that resets the CGI and browser to a known state."""
-    return '''<!--: spam
-Content-Type: text/html
-
-<body bgcolor="#f0f0f8"><font color="#f0f0f8" size="-5"> -->
-<body bgcolor="#f0f0f8"><font color="#f0f0f8" size="-5"> --> -->
-</font> </font> </font> </script> </object> </blockquote> </pre>
-</table> </table> </table> </table> </table> </font> </font> </font>'''
-
-__UNDEF__ = []                          # a special sentinel object
-def small(text):
-    if text:
-        return '<small>' + text + '</small>'
-    else:
-        return ''
-
-def strong(text):
-    if text:
-        return '<strong>' + text + '</strong>'
-    else:
-        return ''
-
-def grey(text):
-    if text:
-        return '<font color="#909090">' + text + '</font>'
-    else:
-        return ''
-
-def lookup(name, frame, locals):
-    """Find the value for a given name in the given environment."""
-    if name in locals:
-        return 'local', locals[name]
-    if name in frame.f_globals:
-        return 'global', frame.f_globals[name]
-    if '__builtins__' in frame.f_globals:
-        builtins = frame.f_globals['__builtins__']
-        if isinstance(builtins, dict):
-            if name in builtins:
-                return 'builtin', builtins[name]
-        else:
-            if hasattr(builtins, name):
-                return 'builtin', getattr(builtins, name)
-    return None, __UNDEF__
-
-def scanvars(reader, frame, locals):
-    """Scan one logical line of Python and look up values of variables used."""
-    vars, lasttoken, parent, prefix, value = [], None, None, '', __UNDEF__
-    for ttype, token, start, end, line in tokenize.generate_tokens(reader):
-        if ttype == tokenize.NEWLINE: break
-        if ttype == tokenize.NAME and token not in keyword.kwlist:
-            if lasttoken == '.':
-                if parent is not __UNDEF__:
-                    value = getattr(parent, token, __UNDEF__)
-                    vars.append((prefix + token, prefix, value))
-            else:
-                where, value = lookup(token, frame, locals)
-                vars.append((token, where, value))
-        elif token == '.':
-            prefix += lasttoken + '.'
-            parent = value
-        else:
-            parent, prefix = None, ''
-        lasttoken = token
-    return vars
-
-def html(einfo, context=5):
-    """Return a nice HTML document describing a given traceback."""
-    etype, evalue, etb = einfo
-    if isinstance(etype, type):
-        etype = etype.__name__
-    pyver = 'Python ' + sys.version.split()[0] + ': ' + sys.executable
-    date = time.ctime(time.time())
-    head = f'''
-<body bgcolor="#f0f0f8">
-<table width="100%" cellspacing=0 cellpadding=2 border=0 summary="heading">
-<tr bgcolor="#6622aa">
-<td valign=bottom> <br>
-<font color="#ffffff" face="helvetica, arial"> <br>
-<big><big><strong>{html_escape(str(etype))}</strong></big></big></font></td>
-<td align=right valign=bottom>
-<font color="#ffffff" face="helvetica, arial">{pyver}<br>{date}</font></td>
-</tr></table>
-<p>A problem occurred in a Python script.  Here is the sequence of
-function calls leading up to the error, in the order they occurred.</p>'''
-
-    indent = '<tt>' + small(' ' * 5) + ' </tt>'
-    frames = []
-    records = inspect.getinnerframes(etb, context)
-    for frame, file, lnum, func, lines, index in records:
-        if file:
-            file = os.path.abspath(file)
-            link = '<a href="file://%s">%s</a>' % (file, pydoc.html.escape(file))
-        else:
-            file = link = '?'
-        args, varargs, varkw, locals = inspect.getargvalues(frame)
-        call = ''
-        if func != '?':
-            call = 'in ' + strong(pydoc.html.escape(func))
-            if func != "<module>":
-                call += inspect.formatargvalues(args, varargs, varkw, locals,
-                    formatvalue=lambda value: '=' + pydoc.html.repr(value))
-
-        highlight = {}
-        def reader(lnum=[lnum]):
-            highlight[lnum[0]] = 1
-            try: return linecache.getline(file, lnum[0])
-            finally: lnum[0] += 1
-        vars = scanvars(reader, frame, locals)
-
-        rows = ['<tr><td bgcolor="#d8bbff">%s%s %s</td></tr>' %
-                ('<big> </big>', link, call)]
-        if index is not None:
-            i = lnum - index
-            for line in lines:
-                num = small(' ' * (5-len(str(i))) + str(i)) + ' '
-                if i in highlight:
-                    line = '<tt>=>%s%s</tt>' % (num, pydoc.html.preformat(line))
-                    rows.append('<tr><td bgcolor="#ffccee">%s</td></tr>' % line)
-                else:
-                    line = '<tt>  %s%s</tt>' % (num, pydoc.html.preformat(line))
-                    rows.append('<tr><td>%s</td></tr>' % grey(line))
-                i += 1
-
-        done, dump = {}, []
-        for name, where, value in vars:
-            if name in done: continue
-            done[name] = 1
-            if value is not __UNDEF__:
-                if where in ('global', 'builtin'):
-                    name = ('<em>%s</em> ' % where) + strong(name)
-                elif where == 'local':
-                    name = strong(name)
-                else:
-                    name = where + strong(name.split('.')[-1])
-                dump.append('%s = %s' % (name, pydoc.html.repr(value)))
-            else:
-                dump.append(name + ' <em>undefined</em>')
-
-        rows.append('<tr><td>%s</td></tr>' % small(grey(', '.join(dump))))
-        frames.append('''
-<table width="100%%" cellspacing=0 cellpadding=0 border=0>
-%s</table>''' % '\n'.join(rows))
-
-    exception = ['<p>%s: %s' % (strong(pydoc.html.escape(str(etype))),
-                                pydoc.html.escape(str(evalue)))]
-    for name in dir(evalue):
-        if name[:1] == '_': continue
-        value = pydoc.html.repr(getattr(evalue, name))
-        exception.append('\n<br>%s%s =\n%s' % (indent, name, value))
-
-    return head + ''.join(frames) + ''.join(exception) + '''
-
-
-<!-- The above is a description of an error in a Python program, formatted
-     for a web browser because the 'cgitb' module was enabled.  In case you
-     are not reading this in a web browser, here is the original traceback:
-
-%s
--->
-''' % pydoc.html.escape(
-          ''.join(traceback.format_exception(etype, evalue, etb)))
-
-def text(einfo, context=5):
-    """Return a plain text document describing a given traceback."""
-    etype, evalue, etb = einfo
-    if isinstance(etype, type):
-        etype = etype.__name__
-    pyver = 'Python ' + sys.version.split()[0] + ': ' + sys.executable
-    date = time.ctime(time.time())
-    head = "%s\n%s\n%s\n" % (str(etype), pyver, date) + '''
-A problem occurred in a Python script.  Here is the sequence of
-function calls leading up to the error, in the order they occurred.
-'''
-
-    frames = []
-    records = inspect.getinnerframes(etb, context)
-    for frame, file, lnum, func, lines, index in records:
-        file = file and os.path.abspath(file) or '?'
-        args, varargs, varkw, locals = inspect.getargvalues(frame)
-        call = ''
-        if func != '?':
-            call = 'in ' + func
-            if func != "<module>":
-                call += inspect.formatargvalues(args, varargs, varkw, locals,
-                    formatvalue=lambda value: '=' + pydoc.text.repr(value))
-
-        highlight = {}
-        def reader(lnum=[lnum]):
-            highlight[lnum[0]] = 1
-            try: return linecache.getline(file, lnum[0])
-            finally: lnum[0] += 1
-        vars = scanvars(reader, frame, locals)
-
-        rows = [' %s %s' % (file, call)]
-        if index is not None:
-            i = lnum - index
-            for line in lines:
-                num = '%5d ' % i
-                rows.append(num+line.rstrip())
-                i += 1
-
-        done, dump = {}, []
-        for name, where, value in vars:
-            if name in done: continue
-            done[name] = 1
-            if value is not __UNDEF__:
-                if where == 'global': name = 'global ' + name
-                elif where != 'local': name = where + name.split('.')[-1]
-                dump.append('%s = %s' % (name, pydoc.text.repr(value)))
-            else:
-                dump.append(name + ' undefined')
-
-        rows.append('\n'.join(dump))
-        frames.append('\n%s\n' % '\n'.join(rows))
-
-    exception = ['%s: %s' % (str(etype), str(evalue))]
-    for name in dir(evalue):
-        value = pydoc.text.repr(getattr(evalue, name))
-        exception.append('\n%s%s = %s' % (" "*4, name, value))
-
-    return head + ''.join(frames) + ''.join(exception) + '''
-
-The above is a description of an error in a Python program.  Here is
-the original traceback:
-
-%s
-''' % ''.join(traceback.format_exception(etype, evalue, etb))
-
-class Hook:
-    """A hook to replace sys.excepthook that shows tracebacks in HTML."""
-
-    def __init__(self, display=1, logdir=None, context=5, file=None,
-                 format="html"):
-        self.display = display          # send tracebacks to browser if true
-        self.logdir = logdir            # log tracebacks to files if not None
-        self.context = context          # number of source code lines per frame
-        self.file = file or sys.stdout  # place to send the output
-        self.format = format
-
-    def __call__(self, etype, evalue, etb):
-        self.handle((etype, evalue, etb))
-
-    def handle(self, info=None):
-        info = info or sys.exc_info()
-        if self.format == "html":
-            self.file.write(reset())
-
-        formatter = (self.format=="html") and html or text
-        plain = False
-        try:
-            doc = formatter(info, self.context)
-        except:                         # just in case something goes wrong
-            doc = ''.join(traceback.format_exception(*info))
-            plain = True
-
-        if self.display:
-            if plain:
-                doc = pydoc.html.escape(doc)
-                self.file.write('<pre>' + doc + '</pre>\n')
-            else:
-                self.file.write(doc + '\n')
-        else:
-            self.file.write('<p>A problem occurred in a Python script.\n')
-
-        if self.logdir is not None:
-            suffix = ['.txt', '.html'][self.format=="html"]
-            (fd, path) = tempfile.mkstemp(suffix=suffix, dir=self.logdir)
-
-            try:
-                with os.fdopen(fd, 'w') as file:
-                    file.write(doc)
-                msg = '%s contains the description of this error.' % path
-            except:
-                msg = 'Tried to save traceback to %s, but failed.' % path
-
-            if self.format == 'html':
-                self.file.write('<p>%s</p>\n' % msg)
-            else:
-                self.file.write(msg + '\n')
-        try:
-            self.file.flush()
-        except: pass
-
-handler = Hook().handle
-def enable(display=1, logdir=None, context=5, format="html"):
-    """Install an exception handler that formats tracebacks as HTML.
-
-    The optional argument 'display' can be set to 0 to suppress sending the
-    traceback to the browser, and 'logdir' can be set to a directory to cause
-    tracebacks to be written to files there."""
-    sys.excepthook = Hook(display=display, logdir=logdir,
-                          context=context, format=format)
diff --git a/Lib/test/test_cgi.py b/Lib/test/test_cgi.py
deleted file mode 100644
index 24486e4d95a7..000000000000
--- a/Lib/test/test_cgi.py
+++ /dev/null
@@ -1,641 +0,0 @@
-import os
-import sys
-import tempfile
-import unittest
-from collections import namedtuple
-from io import StringIO, BytesIO
-from test import support
-from test.support import warnings_helper
-
-cgi = warnings_helper.import_deprecated("cgi")
-
-
-class HackedSysModule:
-    # The regression test will have real values in sys.argv, which
-    # will completely confuse the test of the cgi module
-    argv = []
-    stdin = sys.stdin
-
-cgi.sys = HackedSysModule()
-
-class ComparableException:
-    def __init__(self, err):
-        self.err = err
-
-    def __str__(self):
-        return str(self.err)
-
-    def __eq__(self, anExc):
-        if not isinstance(anExc, Exception):
-            return NotImplemented
-        return (self.err.__class__ == anExc.__class__ and
-                self.err.args == anExc.args)
-
-    def __getattr__(self, attr):
-        return getattr(self.err, attr)
-
-def do_test(buf, method):
-    env = {}
-    if method == "GET":
-        fp = None
-        env['REQUEST_METHOD'] = 'GET'
-        env['QUERY_STRING'] = buf
-    elif method == "POST":
-        fp = BytesIO(buf.encode('latin-1')) # FieldStorage expects bytes
-        env['REQUEST_METHOD'] = 'POST'
-        env['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'
-        env['CONTENT_LENGTH'] = str(len(buf))
-    else:
-        raise ValueError("unknown method: %s" % method)
-    try:
-        return cgi.parse(fp, env, strict_parsing=1)
-    except Exception as err:
-        return ComparableException(err)
-
-parse_strict_test_cases = [
-    ("", {}),
-    ("&", ValueError("bad query field: ''")),
-    ("&&", ValueError("bad query field: ''")),
-    # Should the next few really be valid?
-    ("=", {}),
-    ("=&=", {}),
-    # This rest seem to make sense
-    ("=a", {'': ['a']}),
-    ("&=a", ValueError("bad query field: ''")),
-    ("=a&", ValueError("bad query field: ''")),
-    ("=&a", ValueError("bad query field: 'a'")),
-    ("b=a", {'b': ['a']}),
-    ("b+=a", {'b ': ['a']}),
-    ("a=b=a", {'a': ['b=a']}),
-    ("a=+b=a", {'a': [' b=a']}),
-    ("&b=a", ValueError("bad query field: ''")),
-    ("b&=a", ValueError("bad query field: 'b'")),
-    ("a=a+b&b=b+c", {'a': ['a b'], 'b': ['b c']}),
-    ("a=a+b&a=b+a", {'a': ['a b', 'b a']}),
-    ("x=1&y=2.0&z=2-3.%2b0", {'x': ['1'], 'y': ['2.0'], 'z': ['2-3.+0']}),
-    ("Hbc5161168c542333633315dee1182227:key_store_seqid=400006&cuyer=r&view=bustomer&order_id=0bb2e248638833d48cb7fed300000f1b&expire=964546263&lobale=en-US&kid=130003.300038&ss=env",
-     {'Hbc5161168c542333633315dee1182227:key_store_seqid': ['400006'],
-      'cuyer': ['r'],
-      'expire': ['964546263'],
-      'kid': ['130003.300038'],
-      'lobale': ['en-US'],
-      'order_id': ['0bb2e248638833d48cb7fed300000f1b'],
-      'ss': ['env'],
-      'view': ['bustomer'],
-      }),
-
-    ("group_id=5470&set=custom&_assigned_to=31392&_status=1&_category=100&SUBMIT=Browse",
-     {'SUBMIT': ['Browse'],
-      '_assigned_to': ['31392'],
-      '_category': ['100'],
-      '_status': ['1'],
-      'group_id': ['5470'],
-      'set': ['custom'],
-      })
-    ]
-
-def norm(seq):
-    return sorted(seq, key=repr)
-
-def first_elts(list):
-    return [p[0] for p in list]
-
-def first_second_elts(list):
-    return [(p[0], p[1][0]) for p in list]
-
-def gen_result(data, environ):
-    encoding = 'latin-1'
-    fake_stdin = BytesIO(data.encode(encoding))
-    fake_stdin.seek(0)
-    form = cgi.FieldStorage(fp=fake_stdin, environ=environ, encoding=encoding)
-
-    result = {}
-    for k, v in dict(form).items():
-        result[k] = isinstance(v, list) and form.getlist(k) or v.value
-
-    return result
-
-class CgiTests(unittest.TestCase):
-
-    def test_parse_multipart(self):
-        fp = BytesIO(POSTDATA.encode('latin1'))
-        env = {'boundary': BOUNDARY.encode('latin1'),
-               'CONTENT-LENGTH': '558'}
-        result = cgi.parse_multipart(fp, env)
-        expected = {'submit': [' Add '], 'id': ['1234'],
-                    'file': [b'Testing 123.\n'], 'title': ['']}
-        self.assertEqual(result, expected)
-
-    def test_parse_multipart_without_content_length(self):
-        POSTDATA = '''--JfISa01
-Content-Disposition: form-data; name="submit-name"
-
-just a string
-
---JfISa01--
-'''
-        fp = BytesIO(POSTDATA.encode('latin1'))
-        env = {'boundary': 'JfISa01'.encode('latin1')}
-        result = cgi.parse_multipart(fp, env)
-        expected = {'submit-name': ['just a string\n']}
-        self.assertEqual(result, expected)
-
-    def test_parse_multipart_invalid_encoding(self):
-        BOUNDARY = "JfISa01"
-        POSTDATA = """--JfISa01
-Content-Disposition: form-data; name="submit-name"
-Content-Length: 3
-
-\u2603
---JfISa01"""
-        fp = BytesIO(POSTDATA.encode('utf8'))
-        env = {'boundary': BOUNDARY.encode('latin1'),
-               'CONTENT-LENGTH': str(len(POSTDATA.encode('utf8')))}
-        result = cgi.parse_multipart(fp, env, encoding="ascii",
-                                     errors="surrogateescape")
-        expected = {'submit-name': ["\udce2\udc98\udc83"]}
-        self.assertEqual(result, expected)
-        self.assertEqual("\u2603".encode('utf8'),
-                         result["submit-name"][0].encode('utf8', 'surrogateescape'))
-
-    def test_fieldstorage_properties(self):
-        fs = cgi.FieldStorage()
-        self.assertFalse(fs)
-        self.assertIn("FieldStorage", repr(fs))
-        self.assertEqual(list(fs), list(fs.keys()))
-        fs.list.append(namedtuple('MockFieldStorage', 'name')('fieldvalue'))
-        self.assertTrue(fs)
-
-    def test_fieldstorage_invalid(self):
-        self.assertRaises(TypeError, cgi.FieldStorage, "not-a-file-obj",
-                                                            environ={"REQUEST_METHOD":"PUT"})
-        self.assertRaises(TypeError, cgi.FieldStorage, "foo", "bar")
-        fs = cgi.FieldStorage(headers={'content-type':'text/plain'})
-        self.assertRaises(TypeError, bool, fs)
-
-    def test_strict(self):
-        for orig, expect in parse_strict_test_cases:
-            # Test basic parsing
-            d = do_test(orig, "GET")
-            self.assertEqual(d, expect, "Error parsing %s method GET" % repr(orig))
-            d = do_test(orig, "POST")
-            self.assertEqual(d, expect, "Error parsing %s method POST" % repr(orig))
-
-            env = {'QUERY_STRING': orig}
-            fs = cgi.FieldStorage(environ=env)
-            if isinstance(expect, dict):
-                # test dict interface
-                self.assertEqual(len(expect), len(fs))
-                self.assertCountEqual(expect.keys(), fs.keys())
-                ##self.assertEqual(norm(expect.values()), norm(fs.values()))
-                ##self.assertEqual(norm(expect.items()), norm(fs.items()))
-                self.assertEqual(fs.getvalue("nonexistent field", "default"), "default")
-                # test individual fields
-                for key in expect.keys():
-                    expect_val = expect[key]
-                    self.assertIn(key, fs)
-                    if len(expect_val) > 1:
-                        self.assertEqual(fs.getvalue(key), expect_val)
-                    else:
-                        self.assertEqual(fs.getvalue(key), expect_val[0])
-
-    def test_separator(self):
-        parse_semicolon = [
-            ("x=1;y=2.0", {'x': ['1'], 'y': ['2.0']}),
-            ("x=1;y=2.0;z=2-3.%2b0", {'x': ['1'], 'y': ['2.0'], 'z': ['2-3.+0']}),
-            (";", ValueError("bad query field: ''")),
-            (";;", ValueError("bad query field: ''")),
-            ("=;a", ValueError("bad query field: 'a'")),
-            (";b=a", ValueError("bad query field: ''")),
-            ("b;=a", ValueError("bad query field: 'b'")),
-            ("a=a+b;b=b+c", {'a': ['a b'], 'b': ['b c']}),
-            ("a=a+b;a=b+a", {'a': ['a b', 'b a']}),
-        ]
-        for orig, expect in parse_semicolon:
-            env = {'QUERY_STRING': orig}
-            fs = cgi.FieldStorage(separator=';', environ=env)
-            if isinstance(expect, dict):
-                for key in expect.keys():
-                    expect_val = expect[key]
-                    self.assertIn(key, fs)
-                    if len(expect_val) > 1:
-                        self.assertEqual(fs.getvalue(key), expect_val)
-                    else:
-                        self.assertEqual(fs.getvalue(key), expect_val[0])
-
-    @warnings_helper.ignore_warnings(category=DeprecationWarning)
-    def test_log(self):
-        cgi.log("Testing")
-
-        cgi.logfp = StringIO()
-        cgi.initlog("%s", "Testing initlog 1")
-        cgi.log("%s", "Testing log 2")
-        self.assertEqual(cgi.logfp.getvalue(), "Testing initlog 1\nTesting log 2\n")
-        if os.path.exists(os.devnull):
-            cgi.logfp = None
-            cgi.logfile = os.devnull
-            cgi.initlog("%s", "Testing log 3")
-            self.addCleanup(cgi.closelog)
-            cgi.log("Testing log 4")
-
-    def test_fieldstorage_readline(self):
-        # FieldStorage uses readline, which has the capacity to read all
-        # contents of the input file into memory; we use readline's size argument
-        # to prevent that for files that do not contain any newlines in
-        # non-GET/HEAD requests
-        class TestReadlineFile:
-            def __init__(self, file):
-                self.file = file
-                self.numcalls = 0
-
-            def readline(self, size=None):
-                self.numcalls += 1
-                if size:
-                    return self.file.readline(size)
-                else:
-                    return self.file.readline()
-
-            def __getattr__(self, name):
-                file = self.__dict__['file']
-                a = getattr(file, name)
-                if not isinstance(a, int):
-                    setattr(self, name, a)
-                return a
-
-        f = TestReadlineFile(tempfile.TemporaryFile("wb+"))
-        self.addCleanup(f.close)
-        f.write(b'x' * 256 * 1024)
-        f.seek(0)
-        env = {'REQUEST_METHOD':'PUT'}
-        fs = cgi.FieldStorage(fp=f, environ=env)
-        self.addCleanup(fs.file.close)
-        # if we're not chunking properly, readline is only called twice
-        # (by read_binary); if we are chunking properly, it will be called 5 times
-        # as long as the chunksize is 1 << 16.
-        self.assertGreater(f.numcalls, 2)
-        f.close()
-
-    def test_fieldstorage_multipart(self):
-        #Test basic FieldStorage multipart parsing
-        env = {
-            'REQUEST_METHOD': 'POST',
-            'CONTENT_TYPE': 'multipart/form-data; boundary={}'.format(BOUNDARY),
-            'CONTENT_LENGTH': '558'}
-        fp = BytesIO(POSTDATA.encode('latin-1'))
-        fs = cgi.FieldStorage(fp, environ=env, encoding="latin-1")
-        self.assertEqual(len(fs.list), 4)
-        expect = [{'name':'id', 'filename':None, 'value':'1234'},
-                  {'name':'title', 'filename':None, 'value':''},
-                  {'name':'file', 'filename':'test.txt', 'value':b'Testing 123.\n'},
-                  {'name':'submit', 'filename':None, 'value':' Add '}]
-        for x in range(len(fs.list)):
-            for k, exp in expect[x].items():
-                got = getattr(fs.list[x], k)
-                self.assertEqual(got, exp)
-
-    def test_fieldstorage_multipart_leading_whitespace(self):
-        env = {
-            'REQUEST_METHOD': 'POST',
-            'CONTENT_TYPE': 'multipart/form-data; boundary={}'.format(BOUNDARY),
-            'CONTENT_LENGTH': '560'}
-        # Add some leading whitespace to our post data that will cause the
-        # first line to not be the innerboundary.
-        fp = BytesIO(b"\r\n" + POSTDATA.encode('latin-1'))
-        fs = cgi.FieldStorage(fp, environ=env, encoding="latin-1")
-        self.assertEqual(len(fs.list), 4)
-        expect = [{'name':'id', 'filename':None, 'value':'1234'},
-                  {'name':'title', 'filename':None, 'value':''},
-                  {'name':'file', 'filename':'test.txt', 'value':b'Testing 123.\n'},
-                  {'name':'submit', 'filename':None, 'value':' Add '}]
-        for x in range(len(fs.list)):
-            for k, exp in expect[x].items():
-                got = getattr(fs.list[x], k)
-                self.assertEqual(got, exp)
-
-    def test_fieldstorage_multipart_non_ascii(self):
-        #Test basic FieldStorage multipart parsing
-        env = {'REQUEST_METHOD':'POST',
-            'CONTENT_TYPE': 'multipart/form-data; boundary={}'.format(BOUNDARY),
-            'CONTENT_LENGTH':'558'}
-        for encoding in ['iso-8859-1','utf-8']:
-            fp = BytesIO(POSTDATA_NON_ASCII.encode(encoding))
-            fs = cgi.FieldStorage(fp, environ=env,encoding=encoding)
-            self.assertEqual(len(fs.list), 1)
-            expect = [{'name':'id', 'filename':None, 'value':'\xe7\xf1\x80'}]
-            for x in range(len(fs.list)):
-                for k, exp in expect[x].items():
-                    got = getattr(fs.list[x], k)
-                    self.assertEqual(got, exp)
-
-    def test_fieldstorage_multipart_maxline(self):
-        # Issue #18167
-        maxline = 1 << 16
-        self.maxDiff = None
-        def check(content):
-            data = """---123
-Content-Disposition: form-data; name="upload"; filename="fake.txt"
-Content-Type: text/plain
-
-%s
----123--
-""".replace('\n', '\r\n') % content
-            environ = {
-                'CONTENT_LENGTH':   str(len(data)),
-                'CONTENT_TYPE':     'multipart/form-data; boundary=-123',
-                'REQUEST_METHOD':   'POST',
-            }
-            self.assertEqual(gen_result(data, environ),
-                             {'upload': content.encode('latin1')})
-        check('x' * (maxline - 1))
-        check('x' * (maxline - 1) + '\r')
-        check('x' * (maxline - 1) + '\r' + 'y' * (maxline - 1))
-
-    def test_fieldstorage_multipart_w3c(self):
-        # Test basic FieldStorage multipart parsing (W3C sample)
-        env = {
-            'REQUEST_METHOD': 'POST',
-            'CONTENT_TYPE': 'multipart/form-data; boundary={}'.format(BOUNDARY_W3),
-            'CONTENT_LENGTH': str(len(POSTDATA_W3))}
-        fp = BytesIO(POSTDATA_W3.encode('latin-1'))
-        fs = cgi.FieldStorage(fp, environ=env, encoding="latin-1")
-        self.assertEqual(len(fs.list), 2)
-        self.assertEqual(fs.list[0].name, 'submit-name')
-        self.assertEqual(fs.list[0].value, 'Larry')
-        self.assertEqual(fs.list[1].name, 'files')
-        files = fs.list[1].value
-        self.assertEqual(len(files), 2)
-        expect = [{'name': None, 'filename': 'file1.txt', 'value': b'... contents of file1.txt ...'},
-                  {'name': None, 'filename': 'file2.gif', 'value': b'...contents of file2.gif...'}]
-        for x in range(len(files)):
-            for k, exp in expect[x].items():
-                got = getattr(files[x], k)
-                self.assertEqual(got, exp)
-
-    def test_fieldstorage_part_content_length(self):
-        BOUNDARY = "JfISa01"
-        POSTDATA = """--JfISa01
-Content-Disposition: form-data; name="submit-name"
-Content-Length: 5
-
-Larry
---JfISa01"""
-        env = {
-            'REQUEST_METHOD': 'POST',
-            'CONTENT_TYPE': 'multipart/form-data; boundary={}'.format(BOUNDARY),
-            'CONTENT_LENGTH': str(len(POSTDATA))}
-        fp = BytesIO(POSTDATA.encode('latin-1'))
-        fs = cgi.FieldStorage(fp, environ=env, encoding="latin-1")
-        self.assertEqual(len(fs.list), 1)
-        self.assertEqual(fs.list[0].name, 'submit-name')
-        self.assertEqual(fs.list[0].value, 'Larry')
-
-    def test_field_storage_multipart_no_content_length(self):
-        fp = BytesIO(b"""--MyBoundary
-Content-Disposition: form-data; name="my-arg"; filename="foo"
-
-Test
-
---MyBoundary--
-""")
-        env = {
-            "REQUEST_METHOD": "POST",
-            "CONTENT_TYPE": "multipart/form-data; boundary=MyBoundary",
-            "wsgi.input": fp,
-        }
-        fields = cgi.FieldStorage(fp, environ=env)
-
-        self.assertEqual(len(fields["my-arg"].file.read()), 5)
-
-    def test_fieldstorage_as_context_manager(self):
-        fp = BytesIO(b'x' * 10)
-        env = {'REQUEST_METHOD': 'PUT'}
-        with cgi.FieldStorage(fp=fp, environ=env) as fs:
-            content = fs.file.read()
-            self.assertFalse(fs.file.closed)
-        self.assertTrue(fs.file.closed)
-        self.assertEqual(content, 'x' * 10)
-        with self.assertRaisesRegex(ValueError, 'I/O operation on closed file'):
-            fs.file.read()
-
-    _qs_result = {
-        'key1': 'value1',
-        'key2': ['value2x', 'value2y'],
-        'key3': 'value3',
-        'key4': 'value4'
-    }
-    def testQSAndUrlEncode(self):
-        data = "key2=value2x&key3=value3&key4=value4"
-        environ = {
-            'CONTENT_LENGTH':   str(len(data)),
-            'CONTENT_TYPE':     'application/x-www-form-urlencoded',
-            'QUERY_STRING':     'key1=value1&key2=value2y',
-            'REQUEST_METHOD':   'POST',
-        }
-        v = gen_result(data, environ)
-        self.assertEqual(self._qs_result, v)
-
-    def test_max_num_fields(self):
-        # For application/x-www-form-urlencoded
-        data = '&'.join(['a=a']*11)
-        environ = {
-            'CONTENT_LENGTH': str(len(data)),
-            'CONTENT_TYPE': 'application/x-www-form-urlencoded',
-            'REQUEST_METHOD': 'POST',
-        }
-
-        with self.assertRaises(ValueError):
-            cgi.FieldStorage(
-                fp=BytesIO(data.encode()),
-                environ=environ,
-                max_num_fields=10,
-            )
-
-        # For multipart/form-data
-        data = """---123
-Content-Disposition: form-data; name="a"
-
-3
----123
-Content-Type: application/x-www-form-urlencoded
-
-a=4
----123
-Content-Type: application/x-www-form-urlencoded
-
-a=5
----123--
-"""
-        environ = {
-            'CONTENT_LENGTH':   str(len(data)),
-            'CONTENT_TYPE':     'multipart/form-data; boundary=-123',
-            'QUERY_STRING':     'a=1&a=2',
-            'REQUEST_METHOD':   'POST',
-        }
-
-        # 2 GET entities
-        # 1 top level POST entities
-        # 1 entity within the second POST entity
-        # 1 entity within the third POST entity
-        with self.assertRaises(ValueError):
-            cgi.FieldStorage(
-                fp=BytesIO(data.encode()),
-                environ=environ,
-                max_num_fields=4,
-            )
-        cgi.FieldStorage(
-            fp=BytesIO(data.encode()),
-            environ=environ,
-            max_num_fields=5,
-        )
-
-    def testQSAndFormData(self):
-        data = """---123
-Content-Disposition: form-data; name="key2"
-
-value2y
----123
-Content-Disposition: form-data; name="key3"
-
-value3
----123
-Content-Disposition: form-data; name="key4"
-
-value4
----123--
-"""
-        environ = {
-            'CONTENT_LENGTH':   str(len(data)),
-            'CONTENT_TYPE':     'multipart/form-data; boundary=-123',
-            'QUERY_STRING':     'key1=value1&key2=value2x',
-            'REQUEST_METHOD':   'POST',
-        }
-        v = gen_result(data, environ)
-        self.assertEqual(self._qs_result, v)
-
-    def testQSAndFormDataFile(self):
-        data = """---123
-Content-Disposition: form-data; name="key2"
-
-value2y
----123
-Content-Disposition: form-data; name="key3"
-
-value3
----123
-Content-Disposition: form-data; name="key4"
-
-value4
----123
-Content-Disposition: form-data; name="upload"; filename="fake.txt"
-Content-Type: text/plain
-
-this is the content of the fake file
-
----123--
-"""
-        environ = {
-            'CONTENT_LENGTH':   str(len(data)),
-            'CONTENT_TYPE':     'multipart/form-data; boundary=-123',
-            'QUERY_STRING':     'key1=value1&key2=value2x',
-            'REQUEST_METHOD':   'POST',
-        }
-        result = self._qs_result.copy()
-        result.update({
-            'upload': b'this is the content of the fake file\n'
-        })
-        v = gen_result(data, environ)
-        self.assertEqual(result, v)
-
-    def test_parse_header(self):
-        self.assertEqual(
-            cgi.parse_header("text/plain"),
-            ("text/plain", {}))
-        self.assertEqual(
-            cgi.parse_header("text/vnd.just.made.this.up ; "),
-            ("text/vnd.just.made.this.up", {}))
-        self.assertEqual(
-            cgi.parse_header("text/plain;charset=us-ascii"),
-            ("text/plain", {"charset": "us-ascii"}))
-        self.assertEqual(
-            cgi.parse_header('text/plain ; charset="us-ascii"'),
-            ("text/plain", {"charset": "us-ascii"}))
-        self.assertEqual(
-            cgi.parse_header('text/plain ; charset="us-ascii"; another=opt'),
-            ("text/plain", {"charset": "us-ascii", "another": "opt"}))
-        self.assertEqual(
-            cgi.parse_header('attachment; filename="silly.txt"'),
-            ("attachment", {"filename": "silly.txt"}))
-        self.assertEqual(
-            cgi.parse_header('attachment; filename="strange;name"'),
-            ("attachment", {"filename": "strange;name"}))
-        self.assertEqual(
-            cgi.parse_header('attachment; filename="strange;name";size=123;'),
-            ("attachment", {"filename": "strange;name", "size": "123"}))
-        self.assertEqual(
-            cgi.parse_header('form-data; name="files"; filename="fo\\"o;bar"'),
-            ("form-data", {"name": "files", "filename": 'fo"o;bar'}))
-
-    def test_all(self):
-        not_exported = {
-            "logfile", "logfp", "initlog", "dolog", "nolog", "closelog", "log",
-            "maxlen", "valid_boundary"}
-        support.check__all__(self, cgi, not_exported=not_exported)
-
-
-BOUNDARY = "---------------------------721837373350705526688164684"
-
-POSTDATA = """-----------------------------721837373350705526688164684
-Content-Disposition: form-data; name="id"
-
-1234
------------------------------721837373350705526688164684
-Content-Disposition: form-data; name="title"
-
-
------------------------------721837373350705526688164684
-Content-Disposition: form-data; name="file"; filename="test.txt"
-Content-Type: text/plain
-
-Testing 123.
-
------------------------------721837373350705526688164684
-Content-Disposition: form-data; name="submit"
-
- Add\x20
------------------------------721837373350705526688164684--
-"""
-
-POSTDATA_NON_ASCII = """-----------------------------721837373350705526688164684
-Content-Disposition: form-data; name="id"
-
-\xe7\xf1\x80
------------------------------721837373350705526688164684
-"""
-
-# http://www.w3.org/TR/html401/interact/forms.html#h-17.13.4
-BOUNDARY_W3 = "AaB03x"
-POSTDATA_W3 = """--AaB03x
-Content-Disposition: form-data; name="submit-name"
-
-Larry
---AaB03x
-Content-Disposition: form-data; name="files"
-Content-Type: multipart/mixed; boundary=BbC04y
-
---BbC04y
-Content-Disposition: file; filename="file1.txt"
-Content-Type: text/plain
-
-... contents of file1.txt ...
---BbC04y
-Content-Disposition: file; filename="file2.gif"
-Content-Type: image/gif
-Content-Transfer-Encoding: binary
-
-...contents of file2.gif...
---BbC04y--
---AaB03x--
-"""
-
-if __name__ == '__main__':
-    unittest.main()
diff --git a/Lib/test/test_cgitb.py b/Lib/test/test_cgitb.py
deleted file mode 100644
index 501c7fcce28e..000000000000
--- a/Lib/test/test_cgitb.py
+++ /dev/null
@@ -1,71 +0,0 @@
-from test.support.os_helper import temp_dir
-from test.support.script_helper import assert_python_failure
-from test.support.warnings_helper import import_deprecated
-import unittest
-import sys
-cgitb = import_deprecated("cgitb")
-
-class TestCgitb(unittest.TestCase):
-
-    def test_fonts(self):
-        text = "Hello Robbie!"
-        self.assertEqual(cgitb.small(text), "<small>{}</small>".format(text))
-        self.assertEqual(cgitb.strong(text), "<strong>{}</strong>".format(text))
-        self.assertEqual(cgitb.grey(text),
-                         '<font color="#909090">{}</font>'.format(text))
-
-    def test_blanks(self):
-        self.assertEqual(cgitb.small(""), "")
-        self.assertEqual(cgitb.strong(""), "")
-        self.assertEqual(cgitb.grey(""), "")
-
-    def test_html(self):
-        try:
-            raise ValueError("Hello World")
-        except ValueError as err:
-            # If the html was templated we could do a bit more here.
-            # At least check that we get details on what we just raised.
-            html = cgitb.html(sys.exc_info())
-            self.assertIn("ValueError", html)
-            self.assertIn(str(err), html)
-
-    def test_text(self):
-        try:
-            raise ValueError("Hello World")
-        except ValueError:
-            text = cgitb.text(sys.exc_info())
-            self.assertIn("ValueError", text)
-            self.assertIn("Hello World", text)
-
-    def test_syshook_no_logdir_default_format(self):
-        with temp_dir() as tracedir:
-            rc, out, err = assert_python_failure(
-                  '-c',
-                  ('import cgitb; cgitb.enable(logdir=%s); '
-                   'raise ValueError("Hello World")') % repr(tracedir),
-                  PYTHONIOENCODING='utf-8')
-        out = out.decode()
-        self.assertIn("ValueError", out)
-        self.assertIn("Hello World", out)
-        self.assertIn("<strong><module></strong>", out)
-        # By default we emit HTML markup.
-        self.assertIn('<p>', out)
-        self.assertIn('</p>', out)
-
-    def test_syshook_no_logdir_text_format(self):
-        # Issue 12890: we were emitting the <p> tag in text mode.
-        with temp_dir() as tracedir:
-            rc, out, err = assert_python_failure(
-                  '-c',
-                  ('import cgitb; cgitb.enable(format="text", logdir=%s); '
-                   'raise ValueError("Hello World")') % repr(tracedir),
-                  PYTHONIOENCODING='utf-8')
-        out = out.decode()
-        self.assertIn("ValueError", out)
-        self.assertIn("Hello World", out)
-        self.assertNotIn('<p>', out)
-        self.assertNotIn('</p>', out)
-
-
-if __name__ == "__main__":
-    unittest.main()
diff --git a/Lib/test/test_pyclbr.py b/Lib/test/test_pyclbr.py
index 23453e340159..c7c5419ffe3e 100644
--- a/Lib/test/test_pyclbr.py
+++ b/Lib/test/test_pyclbr.py
@@ -219,9 +219,6 @@ def test_others(self):
 
         # These were once some of the longest modules.
         cm('random', ignore=('Random',))  # from _random import Random as CoreGenerator
-        with warnings.catch_warnings():
-            warnings.simplefilter('ignore', DeprecationWarning)
-            cm('cgi', ignore=('log',))      # set with = in module
         cm('pickle', ignore=('partial', 'PickleBuffer'))
         with warnings.catch_warnings():
             warnings.simplefilter('ignore', DeprecationWarning)
diff --git a/Misc/NEWS.d/3.9.0a1.rst b/Misc/NEWS.d/3.9.0a1.rst
index 7da438597318..94c3a37de8ee 100644
--- a/Misc/NEWS.d/3.9.0a1.rst
+++ b/Misc/NEWS.d/3.9.0a1.rst
@@ -3457,7 +3457,7 @@ Patch contributed by Rémi Lapeyre.
 .. nonce: kG0ub5
 .. section: Library
 
-Fixes a bug in :mod:`cgi` module when a multipart/form-data request has no
+Fixes a bug in :mod:`!cgi` module when a multipart/form-data request has no
 `Content-Length` header.
 
 ..
diff --git a/Misc/NEWS.d/next/Library/2023-05-23-01-47-57.gh-issue-104773.I6MQhb.rst b/Misc/NEWS.d/next/Library/2023-05-23-01-47-57.gh-issue-104773.I6MQhb.rst
new file mode 100644
index 000000000000..42f7a5286867
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2023-05-23-01-47-57.gh-issue-104773.I6MQhb.rst
@@ -0,0 +1,2 @@
+:pep:`594`: Remove the :mod:`!cgi`` and :mod:`!cgitb` modules, deprecated in
+Python 3.11. Patch by Victor Stinner.
diff --git a/Python/stdlib_module_names.h b/Python/stdlib_module_names.h
index b926caf4a579..f5defe8923b2 100644
--- a/Python/stdlib_module_names.h
+++ b/Python/stdlib_module_names.h
@@ -107,8 +107,6 @@ static const char* _Py_stdlib_module_names[] = {
 "bz2",
 "cProfile",
 "calendar",
-"cgi",
-"cgitb",
 "chunk",
 "cmath",
 "cmd",
diff --git a/Tools/wasm/wasm_assets.py b/Tools/wasm/wasm_assets.py
index fcd99405ee0d..a2ec54ec9867 100755
--- a/Tools/wasm/wasm_assets.py
+++ b/Tools/wasm/wasm_assets.py
@@ -64,8 +64,6 @@
 # socket.create_connection() raises an exception:
 # "BlockingIOError: [Errno 26] Operation in progress".
 OMIT_NETWORKING_FILES = (
-    "cgi.py",
-    "cgitb.py",
     "email/",
     "ftplib.py",
     "http/",



More information about the Python-checkins mailing list