Repository: twisted/twisted

Issue:
Cleanup docs generation after full Read The Docs migration
We now only publish the documentation via the Read the docs project.

There are still some scripts and tools from the time when they were hosted on a dornkirk

While working on this, also move the dev docs into the root and generate redirect pages.

As part of this we can remove custom them and only use the read the docs theme

Remove trac-links and other plugins


Discussion:

Resolving patch:
diff --git a/docs/_extensions/traclinks.py b/docs/_extensions/traclinks.py
deleted file mode 100644
--- a/docs/_extensions/traclinks.py
+++ /dev/null
@@ -1,34 +0,0 @@
-"""
-Sphinx/docutils extension to create links to a Trac site using a
-RestructuredText interpreted text role that looks like this::
-
-    :trac:`trac_link_text`
-
-for example::
-
-    :trac:`#2015`
-
-creates a link to ticket number 2015.
-
-adapted from recipe U{here <https://stackoverflow.com/a/2111327/13564>}
-"""
-
-import urllib
-
-from docutils import nodes, utils
-
-
-def make_trac_link(name, rawtext, text, lineno, inliner, options={}, content=[]):
-    env = inliner.document.settings.env
-    trac_url = env.config.traclinks_base_url
-    ref = trac_url + "/intertrac/" + urllib.quote(text, safe="")
-    node = nodes.reference(rawtext, utils.unescape(text), refuri=ref, **options)
-    return [node], []
-
-
-# setup function to register the extension
-
-
-def setup(app):
-    app.add_config_value("traclinks_base_url", "https://twistedmatrix.com/trac", "env")
-    app.add_role("trac", make_trac_link)
diff --git a/docs/conf.py b/docs/conf.py
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -18,6 +18,8 @@
 from datetime import date
 from pprint import pprint
 
+import sphinx_rtd_theme
+
 # If extensions (or modules to document with autodoc) are in another directory,
 # add these directories to sys.path here. If the directory is relative to the
 # documentation root, use os.path.abspath to make it absolute, like shown here.
@@ -45,8 +47,6 @@
 except ImportError:
     pass
 
-extensions.append("traclinks")
-
 from twisted import version as twisted_version_object
 
 # Add any paths that contain templates here, relative to this directory.
@@ -96,18 +96,9 @@
 
 # -- Options for HTML output ----------------------------------------------
 
-# The theme to use for HTML and HTML Help pages.  See the documentation for
-# a list of builtin themes.
-
-# See Read The Docs environment variables
-# https://docs.readthedocs.io/en/stable/builds.html#build-environment
-on_rtd = os.environ.get("READTHEDOCS", None) == "True"
-
-if not on_rtd:
-    html_theme = "twistedtrac"
-
-# Add any paths that contain custom themes here, relative to this directory.
-html_theme_path = ["_themes"]
+# Only the RTD theme is used. Both for local development and for the public site.
+html_theme = "sphinx_rtd_theme"
+html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
 
 # Add any paths that contain custom static files (such as style sheets) here,
 # relative to this directory. They are copied after the builtin static files,
@@ -194,18 +185,6 @@
 pprint(dict(os.environ))
 print("======")
 
-# Base url for API docs
-api_base_url = f"/documents/{release}/api/"
-if on_rtd:
-    # For a PR the link is like:
-    # https://twisted--1422.org.readthedocs.build/en/1422/
-    # For a release:
-    # https://docs.twistedmatrix.com/en/twisted-20.3.0/
-    # https://docs.twistedmatrix.com/en/latest/
-    api_base_url = "/{}/{}/api/".format(
-        os.environ["READTHEDOCS_LANGUAGE"],
-        os.environ["READTHEDOCS_VERSION"],
-    )
 
 # Try to find URL fragment for the GitHub source page based on current
 # branch or tag.
@@ -236,8 +215,6 @@
 
 pydoctor_url_path = "/en/{rtd_version}/api/"
 
-traclinks_base_url = "https://twistedmatrix.com/trac"
-
 # A dict mapping unique IDs (which can be used to disambiguate references) to a
 # tuple of (<external sphinx documentation URI>, <inventory file location>).
 # The inventory file may be None to use the default location at the given URI.
diff --git a/src/twisted/python/_release.py b/src/twisted/python/_release.py
--- a/src/twisted/python/_release.py
+++ b/src/twisted/python/_release.py
@@ -20,22 +20,9 @@
 from zope.interface import Interface, implementer
 
 from twisted.python.compat import execfile
-from twisted.python.filepath import FilePath
 
 # Types of newsfragments.
 NEWSFRAGMENT_TYPES = ["doc", "bugfix", "misc", "feature", "removal"]
-intersphinxURLs = [
-    "https://docs.python.org/3/objects.inv",
-    "https://cryptography.io/en/latest/objects.inv",
-    "https://pyopenssl.readthedocs.io/en/stable/objects.inv",
-    "https://hyperlink.readthedocs.io/en/stable/objects.inv",
-    "https://twisted.org/constantly/docs/objects.inv",
-    "https://twisted.org/incremental/docs/objects.inv",
-    "https://python-hyper.org/projects/hyper-h2/en/stable/objects.inv",
-    "https://priority.readthedocs.io/en/stable/objects.inv",
-    "https://zopeinterface.readthedocs.io/en/latest/objects.inv",
-    "https://automat.readthedocs.io/en/latest/objects.inv",
-]
 
 
 def runCommand(args, **kwargs):
@@ -263,88 +250,6 @@ class NoDocumentsFound(Exception):
     """
 
 
-class SphinxBuilder:
-    """
-    Generate HTML documentation using Sphinx.
-
-    Generates and runs a shell command that looks something like::
-
-        sphinx-build -b html -d [BUILDDIR]/doctrees
-                                [DOCDIR]/source
-                                [BUILDDIR]/html
-
-    where DOCDIR is a directory containing another directory called "source"
-    which contains the Sphinx source files, and BUILDDIR is the directory in
-    which the Sphinx output will be created.
-    """
-
-    def main(self, args):
-        """
-        Build the main documentation.
-
-        @type args: list of str
-        @param args: The command line arguments to process.  This must contain
-            one string argument: the path to the root of a Twisted checkout.
-            Additional arguments will be ignored for compatibility with legacy
-            build infrastructure.
-        """
-        output = self.build(FilePath(args[0]).child("docs"))
-        if output:
-            sys.stdout.write(f"Unclean build:\n{output}\n")
-            raise sys.exit(1)
-
-    def build(self, docDir, buildDir=None, version=""):
-        """
-        Build the documentation in C{docDir} with Sphinx.
-
-        @param docDir: The directory of the documentation.  This is a directory
-            which contains another directory called "source" which contains the
-            Sphinx "conf.py" file and sphinx source documents.
-        @type docDir: L{twisted.python.filepath.FilePath}
-
-        @param buildDir: The directory to build the documentation in.  By
-            default this will be a child directory of {docDir} named "build".
-        @type buildDir: L{twisted.python.filepath.FilePath}
-
-        @param version: The version of Twisted to set in the docs.
-        @type version: C{str}
-
-        @return: the output produced by running the command
-        @rtype: L{str}
-        """
-        if buildDir is None:
-            buildDir = docDir.parent().child("doc")
-
-        doctreeDir = buildDir.child("doctrees")
-
-        output = runCommand(
-            [
-                "sphinx-build",
-                "-q",
-                "-b",
-                "html",
-                "-d",
-                doctreeDir.path,
-                docDir.path,
-                buildDir.path,
-            ]
-        ).decode("utf-8")
-
-        # Delete the doctrees, as we don't want them after the docs are built
-        doctreeDir.remove()
-
-        for path in docDir.walk():
-            if path.basename() == "man":
-                segments = path.segmentsFrom(docDir)
-                dest = buildDir
-                while segments:
-                    dest = dest.child(segments.pop(0))
-                if not dest.parent().isdir():
-                    dest.parent().makedirs()
-                path.copyTo(dest)
-        return output
-
-
 def filePathDelta(origin, destination):
     """
     Return a list of strings that represent C{destination} as a path relative


Test patch:
diff --git a/src/twisted/python/test/test_release.py b/src/twisted/python/test/test_release.py
--- a/src/twisted/python/test/test_release.py
+++ b/src/twisted/python/test/test_release.py
@@ -13,10 +13,6 @@
 import shutil
 import sys
 import tempfile
-import textwrap
-from io import StringIO
-from subprocess import CalledProcessError
-from unittest import skipIf
 
 from incremental import Version
 
@@ -27,7 +23,6 @@
     IVCSCommand,
     NotWorkingDirectory,
     Project,
-    SphinxBuilder,
     filePathDelta,
     findTwistedProjects,
     getRepositoryCommand,
@@ -35,7 +30,6 @@
     runCommand,
 )
 from twisted.python.filepath import FilePath
-from twisted.python.procutils import which
 from twisted.trial.unittest import TestCase
 
 if sys.platform != "win32":
@@ -355,160 +349,6 @@ def test_filePathDeltaSimilarEndElements(self):
         )
 
 
-@skipIf(not which("sphinx-build"), "Sphinx not available.")
-class SphinxBuilderTests(TestCase):
-    """
-    Tests for L{SphinxBuilder}.
-
-    @note: This test case depends on twisted.web, which violates the standard
-        Twisted practice of not having anything in twisted.python depend on
-        other Twisted packages and opens up the possibility of creating
-        circular dependencies.  Do not use this as an example of how to
-        structure your dependencies.
-
-    @ivar builder: A plain L{SphinxBuilder}.
-
-    @ivar sphinxDir: A L{FilePath} representing a directory to be used for
-        containing a Sphinx project.
-
-    @ivar sourceDir: A L{FilePath} representing a directory to be used for
-        containing the source files for a Sphinx project.
-    """
-
-    confContent = """\
-                  source_suffix = '.rst'
-                  master_doc = 'index'
-                  """
-    confContent = textwrap.dedent(confContent)
-
-    indexContent = """\
-                   ==============
-                   This is a Test
-                   ==============
-
-                   This is only a test
-                   -------------------
-
-                   In case you hadn't figured it out yet, this is a test.
-                   """
-    indexContent = textwrap.dedent(indexContent)
-
-    def setUp(self):
-        """
-        Set up a few instance variables that will be useful.
-        """
-        self.builder = SphinxBuilder()
-
-        # set up a place for a fake sphinx project
-        self.twistedRootDir = FilePath(self.mktemp())
-        self.sphinxDir = self.twistedRootDir.child("docs")
-        self.sphinxDir.makedirs()
-        self.sourceDir = self.sphinxDir
-
-    def createFakeSphinxProject(self):
-        """
-        Create a fake Sphinx project for test purposes.
-
-        Creates a fake Sphinx project with the absolute minimum of source
-        files.  This includes a single source file ('index.rst') and the
-        smallest 'conf.py' file possible in order to find that source file.
-        """
-        self.sourceDir.child("conf.py").setContent(self.confContent.encode())
-        self.sourceDir.child("index.rst").setContent(self.indexContent.encode())
-
-    def verifyFileExists(self, fileDir, fileName):
-        """
-        Helper which verifies that C{fileName} exists in C{fileDir} and it has
-        some content.
-
-        @param fileDir: A path to a directory.
-        @type fileDir: L{FilePath}
-
-        @param fileName: The last path segment of a file which may exist within
-            C{fileDir}.
-        @type fileName: L{str}
-
-        @raise FailTest: If C{fileDir.child(fileName)}:
-
-                1. Does not exist.
-
-                2. Is empty.
-
-                3. In the case where it's a path to a C{.html} file, the
-                   content looks like an HTML file.
-
-        @return: L{None}
-        """
-        # check that file exists
-        fpath = fileDir.child(fileName)
-        self.assertTrue(fpath.exists())
-
-        # check that the output files have some content
-        fcontents = fpath.getContent()
-        self.assertTrue(len(fcontents) > 0)
-
-        # check that the html files are at least html-ish
-        # this is not a terribly rigorous check
-        if fpath.path.endswith(".html"):
-            self.assertIn(b"<body", fcontents)
-
-    def test_build(self):
-        """
-        Creates and builds a fake Sphinx project using a L{SphinxBuilder}.
-        """
-        self.createFakeSphinxProject()
-        self.builder.build(self.sphinxDir)
-        self.verifyBuilt()
-
-    def test_main(self):
-        """
-        Creates and builds a fake Sphinx project as if via the command line.
-        """
-        self.createFakeSphinxProject()
-        self.builder.main([self.sphinxDir.parent().path])
-        self.verifyBuilt()
-
-    def test_warningsAreErrors(self):
-        """
-        Creates and builds a fake Sphinx project as if via the command line,
-        failing if there are any warnings.
-        """
-        output = StringIO()
-        self.patch(sys, "stdout", output)
-        self.createFakeSphinxProject()
-        with self.sphinxDir.child("index.rst").open("a") as f:
-            f.write(b"\n.. _malformed-link-target\n")
-        exception = self.assertRaises(
-            SystemExit, self.builder.main, [self.sphinxDir.parent().path]
-        )
-        self.assertEqual(exception.code, 1)
-        self.assertIn("malformed hyperlink target", output.getvalue())
-        self.verifyBuilt()
-
-    def verifyBuilt(self):
-        """
-        Verify that a sphinx project has been built.
-        """
-        htmlDir = self.sphinxDir.sibling("doc")
-        self.assertTrue(htmlDir.isdir())
-        doctreeDir = htmlDir.child("doctrees")
-        self.assertFalse(doctreeDir.exists())
-
-        self.verifyFileExists(htmlDir, "index.html")
-        self.verifyFileExists(htmlDir, "genindex.html")
-        self.verifyFileExists(htmlDir, "objects.inv")
-        self.verifyFileExists(htmlDir, "search.html")
-        self.verifyFileExists(htmlDir, "searchindex.js")
-
-    def test_failToBuild(self):
-        """
-        Check that SphinxBuilder.build fails when run against a non-sphinx
-        directory.
-        """
-        # note no fake sphinx project is created
-        self.assertRaises(CalledProcessError, self.builder.build, self.sphinxDir)
-
-
 class CommandsTestMixin(StructureAssertingMixin):
     """
     Test mixin for the VCS commands used by the release scripts.