Repository: biopython/biopython

Issue:
Replacing LaTeX based Tutorial (pdflatex and hevea)?
This replaces old issue https://redmine.open-bio.org/issues/3221 _Port the Biopython Tutorial to Sphinx/reStructuredText_ (filed by @etal), and part of overlapping issue https://redmine.open-bio.org/issues/3219 _Port Biopython documentation to Sphinx_ (also filed by @etal).

We can likely use pandoc as a first pass of converting the current LaTeX source for the tutorial into a more modern markdown language like reStructuredText (`*.rst`, currently used in our docstrings) or MarkDown (`*.md`, currently used for our website).

Any conversion will also need to rewrite `test_Tutorial.py` which pulls doctest formatted examples out of the tutorial source (together with a simple dependency mechanism).



Discussion:
See also #906 about replacing epydoc for generating our API documentation from the docstrings, which are currently written using reStructuredText

Currently we have three markup languages in use:
- Markdown for the website (converted from MediaWiki), plus GitHub issues
- reStructuredText for the API documentation from docstrings (#906)
- LaTeX for the _Tutorial and Cookbook_ (this issue)

Is switching to markdown viable for a document at complex as the current Tutorial? If we switched the docstrings as well (see [pdoc suggestion](https://github.com/biopython/biopython/issues/906#issuecomment-237287500) on #906) this would be much more attractive as a single option.

A relevant comment from @etal on https://redmine.open-bio.org/issues/3221

> While we're at it, we may want to slim down the Tutorial and move some of the more detailed material to a separate Reference based on the automated API docs.

Having both the docstrings and the Tutorial written in the same markup language would make this sort of restructuring much easier in future (regardless of if we use reStructuredText or MarkDown, which seem to be the only sensible choices).

In order to preserve the `test_Tutorial.py` functionality (which I feel has been well worth the time investment so far in both keeping the examples working, but also sometimes catching bugs not covered by the main test suite), we will likely need to define a special comment or markup block.

For markdown https://stackoverflow.com/questions/4823468/comments-in-markdown suggests something like this for a comment:

```
(empty line)
[comment]: # (This actually is the most platform independent comment)
```

Alternatively we might use Markdown's _fenced code blocks_ feature (triple back-ticks) with a bespoke info-string (rather than just python for the colour syntax markup)?

I find this  https://gist.github.com/dupuy/1855764 document interesting, which shows how to write text that can usually be understood by both reStructuredText and Markdown interpreters. When we try to follow these guidelines for the homepage and the docstrings, a later swith to one of the two markup languages may be easier.

With respect to 'slimming down the tutorial' I disagree. I would prefer to have even more examples and details in the tutorial, so that I don't have to switch back and forth between the tutorial, the cookbook sections of the homepage, the wiki entries of the homepage and the api documentation (and the source code for the undocumented functions). So why don't we call the tutorial 'manual' and build it as the main comprehensive Biopython documentation source?

Text which is simultaneously both Markdown and reStructuredText is an intriguing idea, but practically we'd loose out on Python syntax formatted examples etc.

But let's keep this issue on the technical side about what (if anything) we should use instead of LaTeX?

I think that reST would be preferable given its compatibility with Sphinx and Python documentation generally, but on the other hand it looks like there's still no syntax highlighting of Python code blocks in reST files in GitHub. That argues for Markdown. Any new thoughts on this? Happy to help, if an approach is chosen.
I'm leaning very much towards RST - we're already using it for the Python docstrings in the code itself, and the misc text files.
Following the work in #2172 etc to close #906, we now have the API documentation (docstring comments written in RST) converted to HTML documentation using Sphinx.

I have now managed a proof of principle conversion of the Tutorial from LaTeX into RST, and combined this with the API documentation using Sphinx (which could be deployed on our website via GitHub pages, and/or on Read The Docs).

I deliberately kept one page per chapter (i.e. one ``*.rst`` file per chapter, like we have now one ``*.tex`` file per chapter). That has a side effect of complicating links between pages from the LaTeX ``\label{XXX}`` and ``\ref{XXX}``, but I can fix that by converting them to RST style ``:ref:`` entries (and we give up on having chapter and section numbering in the link captions).

I'm unsure at this point if any automated conversion is realistic as part of the continuous integration system. It would be more realistic to aim for a one-off conversion from LaTeX to RST. Thereafter contributors would only need to learn RST for writing or editing the main Biopython documentation.

Resolving patch:
diff --git a/Doc/api/conf.py b/Doc/conf.py
similarity index 94%
rename from Doc/api/conf.py
rename to Doc/conf.py
--- a/Doc/api/conf.py
+++ b/Doc/conf.py
@@ -71,9 +71,9 @@
 prev_minor_version = int(minor_version) - (2 if dev_version else 1)
 previous_version = f"{main_version}.{prev_minor_version}"
 versions = [
-    ("Previous", f"../../{previous_version}/api/"),
-    ("Latest", "../../latest/api/"),
-    ("Develop", "../../dev/api/"),
+    ("Previous", f"../../{previous_version}/"),
+    ("Latest", "../../latest/"),
+    ("Develop", "../../dev/"),
 ]
 
 if version < "1.75":  # 1.74 is the earliest Sphinx-generated api documentation
@@ -89,7 +89,7 @@
 # List of patterns, relative to source directory, that match files and
 # directories to ignore when looking for source files.
 # This patterns also effect to html_static_path and html_extra_path
-exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
+exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "README.rst", "doc.rst"]
 
 # The name of the Pygments (syntax highlighting) style to use.
 pygments_style = "sphinx"
@@ -97,6 +97,10 @@
 # If true, `todo` and `todoList` produce output, else they produce nothing.
 todo_include_todos = True
 
+# If true, figures, tables and code-blocks are automatically numbered if
+# they have a caption. The numref role is enabled.
+numfig = True
+
 # -- Options for autodoc --------------------------------------------------
 
 # This requires Sphinx 1.8 or later:
@@ -138,7 +142,7 @@
     "github_user": "biopython",
     "github_repo": "biopython",
     "github_version": "master",
-    "conf_py_path": "/Doc/api/",
+    "conf_py_path": "/Doc/",
     # "source_suffix": source_suffix,
     "theme_display_version": False,
     # Biopython-specific values for version-footer (versions.html):
@@ -149,7 +153,7 @@
     "project_github_url": "https://github.com/biopython/biopython",
 }
 
-html_logo = "../images/biopython_logo_white.png"
+html_logo = "images/biopython_logo.svg"
 
 # The RST source is transient, don't need/want to include it
 html_show_sourcelink = False
@@ -203,7 +207,9 @@
 # Grouping the document tree into LaTeX files. List of tuples
 # (source start file, target name, title,
 #  author, documentclass [howto, manual, or own class]).
-latex_documents = [(master_doc, "Biopython_API.tex", document, author, "manual")]
+latex_documents = [
+    (master_doc, "Biopython_doc.tex", document, author, "manual"),
+]
 
 
 # -- Options for manual page output ---------------------------------------
@@ -302,7 +308,7 @@ def run_apidoc(_):
     # $ sphinx-apidoc -e -F -o /tmp/api/BioSQL BioSQL
     # $ sphinx-apidoc -e -F -o /tmp/api/Bio Bio
     tmp_path = tempfile.mkdtemp()
-    apidoc_main(["-e", "-F", "-o", tmp_path, "../../BioSQL"])
+    apidoc_main(["-e", "-F", "-o", tmp_path, "../BioSQL"])
     apidoc_main(
         [
             "-e",
@@ -310,17 +316,17 @@ def run_apidoc(_):
             "-o",
             tmp_path,
             # The input path:
-            "../../Bio",
+            "../Bio",
             # These are patterns to exclude:
-            "../../Bio/Alphabet/",
-            "../../Bio/Restriction/Restriction.py",
+            "../Bio/Alphabet/",
+            "../Bio/Restriction/Restriction.py",
         ]
     )
     os.remove(os.path.join(tmp_path, "index.rst"))  # Using our own
     for filename in os.listdir(tmp_path):
         if filename.endswith(".rst"):
             shutil.move(
-                os.path.join(tmp_path, filename), os.path.join(cur_dir, filename)
+                os.path.join(tmp_path, filename), os.path.join(cur_dir, "api", filename)
             )
     shutil.rmtree(tmp_path)
 


Test patch:
diff --git a/Doc/Tutorial/chapter_testing.rst b/Doc/Tutorial/chapter_testing.rst
new file mode 100644
--- /dev/null
+++ b/Doc/Tutorial/chapter_testing.rst
@@ -0,0 +1,443 @@
+.. _`chapter:testing`:
+
+The Biopython testing framework
+===============================
+
+Biopython has a regression testing framework (the file ``run_tests.py``)
+based on `unittest <https://docs.python.org/3/library/unittest.html>`__,
+the standard unit testing framework for Python. Providing comprehensive
+tests for modules is one of the most important aspects of making sure
+that the Biopython code is as bug-free as possible before going out. It
+also tends to be one of the most undervalued aspects of contributing.
+This chapter is designed to make running the Biopython tests and writing
+good test code as easy as possible. Ideally, every module that goes into
+Biopython should have a test (and should also have documentation!). All
+our developers, and anyone installing Biopython from source, are
+strongly encouraged to run the unit tests.
+
+Running the tests
+-----------------
+
+When you download the Biopython source code, or check it out from our
+source code repository, you should find a subdirectory call ``Tests``.
+This contains the key script ``run_tests.py``, lots of individual
+scripts named ``test_XXX.py``, and lots of other subdirectories which
+contain input files for the test suite.
+
+As part of building and installing Biopython you will typically run the
+full test suite at the command line from the Biopython source top level
+directory using the following:
+
+.. code:: console
+
+   $ python setup.py test
+
+This is actually equivalent to going to the ``Tests`` subdirectory and
+running:
+
+.. code:: console
+
+   $ python run_tests.py
+
+You’ll often want to run just some of the tests, and this is done like
+this:
+
+.. code:: console
+
+   $ python run_tests.py test_SeqIO.py test_AlignIO.py
+
+When giving the list of tests, the ``.py`` extension is optional, so you
+can also just type:
+
+.. code:: console
+
+   $ python run_tests.py test_SeqIO test_AlignIO
+
+To run the docstring tests (see section :ref:`sec:doctest`), you can
+use
+
+.. code:: console
+
+   $ python run_tests.py doctest
+
+You can also skip any tests which have been setup with an explicit
+online component by adding ``--offline``, e.g.
+
+.. code:: console
+
+   $ python run_tests.py --offline
+
+By default, ``run_tests.py`` runs all tests, including the docstring
+tests.
+
+If an individual test is failing, you can also try running it directly,
+which may give you more information.
+
+Tests based on Python’s standard ``unittest`` framework will
+``import unittest`` and then define ``unittest.TestCase`` classes, each
+with one or more sub-tests as methods starting with ``test_`` which
+check some specific aspect of the code.
+
+Running the tests using Tox
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Like most Python projects, you can also use
+`Tox <https://tox.readthedocs.org/en/latest/>`__ to run the tests on
+multiple Python versions, provided they are already installed in your
+system.
+
+We do not provide the configuration ``tox.ini`` file in our code base
+because of difficulties pinning down user-specific settings (e.g.
+executable names of the Python versions). You may also only be
+interested in testing Biopython only against a subset of the Python
+versions that we support.
+
+If you are interested in using Tox, you could start with the example
+``tox.ini`` shown below:
+
+.. code:: text
+
+   [tox]
+   envlist = pypy,py38,py39
+
+   [testenv]
+   changedir = Tests
+   commands = {envpython} run_tests.py --offline
+   deps =
+       numpy
+       reportlab
+
+Using the template above, executing ``tox`` will test your Biopython
+code against PyPy, Python 3.8 and 3.9. It assumes that those Pythons’
+executables are named “python3.8“ for Python 3.8, and so on.
+
+Writing tests
+-------------
+
+Let’s say you want to write some tests for a module called ``Biospam``.
+This can be a module you wrote, or an existing module that doesn’t have
+any tests yet. In the examples below, we assume that ``Biospam`` is a
+module that does simple math.
+
+Each Biopython test consists of a script containing the test itself, and
+optionally a directory with input files used by the test:
+
+#. ``test_Biospam.py`` – The actual test code for your module.
+
+#. ``Biospam`` [optional]– A directory where any necessary input files
+   will be located. If you have any output files that should be manually
+   reviewed, output them here (but this is discouraged) to prevent
+   clogging up the main Tests directory. In general, use a temporary
+   file/folder.
+
+Any script with a ``test_`` prefix in the ``Tests`` directory will be
+found and run by ``run_tests.py``. Below, we show an example test script
+``test_Biospam.py``. If you put this script in the Biopython ``Tests``
+directory, then ``run_tests.py`` will find it and execute the tests
+contained in it:
+
+.. code:: console
+
+   $ python run_tests.py
+   test_Ace ... ok
+   test_AlignIO ... ok
+   test_BioSQL ... ok
+   test_BioSQL_SeqIO ... ok
+   test_Biospam ... ok
+   test_CAPS ... ok
+   test_Clustalw ... ok
+   ...
+   ----------------------------------------------------------------------
+   Ran 107 tests in 86.127 seconds
+
+Writing a test using ``unittest``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The ``unittest``-framework has been included with Python since version
+2.1, and is documented in the Python Library Reference (which I know you
+are keeping under your pillow, as recommended). There is also `online
+documentation for
+unittest <https://docs.python.org/3/library/unittest.html>`__. If you
+are familiar with the ``unittest`` system (or something similar like the
+nose test framework), you shouldn’t have any trouble. You may find
+looking at the existing examples within Biopython helpful too.
+
+Here’s a minimal ``unittest``-style test script for ``Biospam``, which
+you can copy and paste to get started:
+
+.. code:: python
+
+   import unittest
+   from Bio import Biospam
+
+
+   class BiospamTestAddition(unittest.TestCase):
+       def test_addition1(self):
+           result = Biospam.addition(2, 3)
+           self.assertEqual(result, 5)
+
+       def test_addition2(self):
+           result = Biospam.addition(9, -1)
+           self.assertEqual(result, 8)
+
+
+   class BiospamTestDivision(unittest.TestCase):
+       def test_division1(self):
+           result = Biospam.division(3.0, 2.0)
+           self.assertAlmostEqual(result, 1.5)
+
+       def test_division2(self):
+           result = Biospam.division(10.0, -2.0)
+           self.assertAlmostEqual(result, -5.0)
+
+
+   if __name__ == "__main__":
+       runner = unittest.TextTestRunner(verbosity=2)
+       unittest.main(testRunner=runner)
+
+In the division tests, we use ``assertAlmostEqual`` instead of
+``assertEqual`` to avoid tests failing due to roundoff errors; see the
+``unittest`` chapter in the Python documentation for details and for
+other functionality available in ``unittest`` (`online
+reference <https://docs.python.org/3/library/unittest.html>`__).
+
+These are the key points of ``unittest``-based tests:
+
+-  Test cases are stored in classes that derive from
+   ``unittest.TestCase`` and cover one basic aspect of your code
+
+-  You can use methods ``setUp`` and ``tearDown`` for any repeated code
+   which should be run before and after each test method. For example,
+   the ``setUp`` method might be used to create an instance of the
+   object you are testing, or open a file handle. The ``tearDown``
+   should do any “tidying up”, for example closing the file handle.
+
+-  The tests are prefixed with ``test_`` and each test should cover one
+   specific part of what you are trying to test. You can have as many
+   tests as you want in a class.
+
+-  At the end of the test script, you can use
+
+   .. code:: python
+
+      if __name__ == "__main__":
+          runner = unittest.TextTestRunner(verbosity=2)
+          unittest.main(testRunner=runner)
+
+   to execute the tests when the script is run by itself (rather than
+   imported from ``run_tests.py``). If you run this script, then you’ll
+   see something like the following:
+
+   .. code:: console
+
+      $ python test_BiospamMyModule.py
+      test_addition1 (__main__.TestAddition) ... ok
+      test_addition2 (__main__.TestAddition) ... ok
+      test_division1 (__main__.TestDivision) ... ok
+      test_division2 (__main__.TestDivision) ... ok
+
+      ----------------------------------------------------------------------
+      Ran 4 tests in 0.059s
+
+      OK
+
+-  To indicate more clearly what each test is doing, you can add
+   docstrings to each test. These are shown when running the tests,
+   which can be useful information if a test is failing.
+
+   .. code:: python
+
+      import unittest
+      from Bio import Biospam
+
+
+      class BiospamTestAddition(unittest.TestCase):
+          def test_addition1(self):
+              """An addition test"""
+              result = Biospam.addition(2, 3)
+              self.assertEqual(result, 5)
+
+          def test_addition2(self):
+              """A second addition test"""
+              result = Biospam.addition(9, -1)
+              self.assertEqual(result, 8)
+
+
+      class BiospamTestDivision(unittest.TestCase):
+          def test_division1(self):
+              """Now let's check division"""
+              result = Biospam.division(3.0, 2.0)
+              self.assertAlmostEqual(result, 1.5)
+
+          def test_division2(self):
+              """A second division test"""
+              result = Biospam.division(10.0, -2.0)
+              self.assertAlmostEqual(result, -5.0)
+
+
+      if __name__ == "__main__":
+          runner = unittest.TextTestRunner(verbosity=2)
+          unittest.main(testRunner=runner)
+
+   Running the script will now show you:
+
+   .. code:: console
+
+      $ python test_BiospamMyModule.py
+      An addition test ... ok
+      A second addition test ... ok
+      Now let's check division ... ok
+      A second division test ... ok
+
+      ----------------------------------------------------------------------
+      Ran 4 tests in 0.001s
+
+      OK
+
+If your module contains docstring tests (see section
+:ref:`sec:doctest`), you *may* want to include those in the tests to
+be run. You can do so as follows by modifying the code under
+``if __name__ == "__main__":`` to look like this:
+
+.. code:: python
+
+   if __name__ == "__main__":
+       unittest_suite = unittest.TestLoader().loadTestsFromName("test_Biospam")
+       doctest_suite = doctest.DocTestSuite(Biospam)
+       suite = unittest.TestSuite((unittest_suite, doctest_suite))
+       runner = unittest.TextTestRunner(sys.stdout, verbosity=2)
+       runner.run(suite)
+
+This is only relevant if you want to run the docstring tests when you
+execute ``python test_Biospam.py`` if it has some complex run-time
+dependency checking.
+
+In general instead include the docstring tests by adding them to the
+``run_tests.py`` as explained below.
+
+.. _`sec:doctest`:
+
+Writing doctests
+----------------
+
+Python modules, classes and functions support built in documentation
+using docstrings. The `doctest
+framework <https://docs.python.org/3/library/doctest.html>`__ (included
+with Python) allows the developer to embed working examples in the
+docstrings, and have these examples automatically tested.
+
+Currently only part of Biopython includes doctests. The ``run_tests.py``
+script takes care of running the doctests. For this purpose, at the top
+of the ``run_tests.py`` script is a manually compiled list of modules to
+skip, important where optional external dependencies which may not be
+installed (e.g. the Reportlab and NumPy libraries). So, if you’ve added
+some doctests to the docstrings in a Biopython module, in order to have
+them excluded in the Biopython test suite, you must update
+``run_tests.py`` to include your module. Currently, the relevant part of
+``run_tests.py`` looks as follows:
+
+.. code:: python
+
+   # Following modules have historic failures. If you fix one of these
+   # please remove here!
+   EXCLUDE_DOCTEST_MODULES = [
+       "Bio.PDB",
+       "Bio.PDB.AbstractPropertyMap",
+       "Bio.Phylo.Applications._Fasttree",
+       "Bio.Phylo._io",
+       "Bio.Phylo.TreeConstruction",
+       "Bio.Phylo._utils",
+   ]
+
+   # Exclude modules with online activity
+   # They are not excluded by default, use --offline to exclude them
+   ONLINE_DOCTEST_MODULES = ["Bio.Entrez", "Bio.ExPASy", "Bio.TogoWS"]
+
+   # Silently ignore any doctests for modules requiring numpy!
+   if numpy is None:
+       EXCLUDE_DOCTEST_MODULES.extend(
+           [
+               "Bio.Affy.CelFile",
+               "Bio.Cluster",
+               # ...
+           ]
+       )
+
+Note that we regard doctests primarily as documentation, so you should
+stick to typical usage. Generally complicated examples dealing with
+error conditions and the like would be best left to a dedicated unit
+test.
+
+Note that if you want to write doctests involving file parsing, defining
+the file location complicates matters. Ideally use relative paths
+assuming the code will be run from the ``Tests`` directory, see the
+``Bio.SeqIO`` doctests for an example of this.
+
+To run the docstring tests only, use
+
+.. code:: console
+
+   $ python run_tests.py doctest
+
+Note that the doctest system is fragile and care is needed to ensure
+your output will match on all the different versions of Python that
+Biopython supports (e.g. differences in floating point numbers).
+
+.. _`sec:doctest-tutorial`:
+
+Writing doctests in the Tutorial
+--------------------------------
+
+This Tutorial you are reading has a lot of code snippets, which are
+often formatted like a doctest. We have our own system in file
+``test_Tutorial.py`` to allow tagging code snippets in the Tutorial
+source to be run as Python doctests. This works by adding special
+``%doctest`` comment lines before each Python block, e.g.
+
+::
+
+
+
+   \pandocignoreme .. doctest
+   \begin{minted}{pycon}
+   >>> from Bio.Seq import Seq
+   >>> s = Seq("ACGT")
+   >>> len(s)
+   4
+   \end{minted}
+
+Often code examples are not self-contained, but continue from the
+previous Python block. Here we use the magic comment ``%cont-doctest``
+as shown here:
+
+::
+
+
+
+   \pandocignoreme .. cont-doctest
+   \begin{minted}{pycon}
+   >>> s == "ACGT"
+   True
+   \end{minted}
+
+The special ``%doctest`` comment line can take a working directory
+(relative to the ``Doc/`` folder) to use if you have any example data
+files, e.g. ``%doctest examples`` will use the ``Doc/examples`` folder,
+while ``%doctest ../Tests/GenBank`` will use the ``Tests/GenBank``
+folder.
+
+After the directory argument, you can specify any Python dependencies
+which must be present in order to run the test by adding ``lib:XXX`` to
+indicate ``import XXX`` must work, e.g. ``%doctest examples lib:numpy``
+
+You can run the Tutorial doctests via:
+
+.. code:: console
+
+   $ python test_Tutorial.py
+
+or:
+
+.. code:: console
+
+   $ python run_tests.py test_Tutorial.py
diff --git a/Doc/Tutorial/chapter_testing.tex b/Doc/Tutorial/chapter_testing.tex
deleted file mode 100644
--- a/Doc/Tutorial/chapter_testing.tex
+++ /dev/null
@@ -1,421 +0,0 @@
-\chapter{The Biopython testing framework}
-\label{chapter:testing}
-
-Biopython has a regression testing framework (the file
-\verb|run_tests.py|) based on
-\href{https://docs.python.org/3/library/unittest.html}{unittest},
-the standard unit testing framework for Python.  Providing comprehensive
-tests for modules is one of the most important aspects of making sure that
-the Biopython code is as bug-free as possible before going out.
-It also tends to be one of the most undervalued aspects of contributing.
-This chapter is designed to make running the Biopython tests and
-writing good test code as easy as possible.
-Ideally, every module that goes into Biopython
-should have a test (and should also have documentation!).
-All our developers, and anyone installing Biopython from source,
-are strongly encouraged to run the unit tests.
-
-\section{Running the tests}
-
-When you download the Biopython source code, or check it out from
-our source code repository, you should find a subdirectory call
-\verb|Tests|.  This contains the key script \verb|run_tests.py|,
-lots of individual scripts named \verb|test_XXX.py|, and lots of
-other subdirectories which contain input files for the test suite.
-
-As part of building and installing Biopython you will typically
-run the full test suite at the command line from the Biopython
-source top level directory using the following:
-
-\begin{minted}{console}
-$ python setup.py test
-\end{minted}
-
-This is actually equivalent to going to the \verb|Tests|
-subdirectory and running:
-
-\begin{minted}{console}
-$ python run_tests.py
-\end{minted}
-
-You'll often want to run just some of the tests, and this is done
-like this:
-
-\begin{minted}{console}
-$ python run_tests.py test_SeqIO.py test_AlignIO.py
-\end{minted}
-
-When giving the list of tests, the \verb|.py| extension is optional,
-so you can also just type:
-
-\begin{minted}{console}
-$ python run_tests.py test_SeqIO test_AlignIO
-\end{minted}
-
-To run the docstring tests (see section \ref{sec:doctest}), you can use
-
-\begin{minted}{console}
-$ python run_tests.py doctest
-\end{minted}
-
-You can also skip any tests which have been setup with an explicit
-online component by adding \verb|--offline|, e.g.
-
-\begin{minted}{console}
-$ python run_tests.py --offline
-\end{minted}
-
-By default, \verb|run_tests.py| runs all tests, including the docstring tests.
-
-If an individual test is failing, you can also try running it
-directly, which may give you more information.
-
-Tests based on Python's standard  \verb|unittest| framework will
-\verb|import unittest| and then define \verb|unittest.TestCase| classes,
-each with one or more sub-tests as methods starting with \verb|test_| which
-check some specific aspect of the code.
-
-\subsection{Running the tests using Tox}
-
-Like most Python projects, you can also use
-\href{https://tox.readthedocs.org/en/latest/}{Tox} to run the tests on multiple
-Python versions, provided they are already installed in your system.
-
-We do not provide the configuration \texttt{tox.ini} file in our code base because
-of difficulties pinning down user-specific settings (e.g. executable names of the
-Python versions). You may also only be interested in testing Biopython only against
-a subset of the Python versions that we support.
-
-If you are interested in using Tox, you could start with the example
-\texttt{tox.ini} shown below:
-
-\begin{minted}{text}
-[tox]
-envlist = pypy,py38,py39
-
-[testenv]
-changedir = Tests
-commands = {envpython} run_tests.py --offline
-deps =
-    numpy
-    reportlab
-\end{minted}
-
-Using the template above, executing \texttt{tox} will test your Biopython
-code against PyPy, Python 3.8 and 3.9. It assumes that those Pythons'
-executables are named ``python3.8`` for Python 3.8, and so on.
-
-
-\section{Writing tests}
-
-Let's say you want to write some tests for a module called \verb|Biospam|.
-This can be a module you wrote, or an existing module that doesn't have
-any tests yet.  In the examples below, we assume that
-\verb|Biospam| is a module that does simple math.
-
-Each Biopython test consists of a script containing the test itself, and
-optionally a directory with input files used by the test:
-
-\begin{enumerate}
-  \item \verb|test_Biospam.py| -- The actual test code for your module.
-  \item \verb|Biospam| [optional]-- A directory where any necessary input files
-    will be located. If you have any output files that should be manually
-    reviewed, output them here (but this is discouraged) to prevent clogging
-    up the main Tests directory. In general, use a temporary file/folder.
-\end{enumerate}
-
-Any script with a \verb|test_| prefix in the \verb|Tests| directory will be found and run by \verb|run_tests.py|. Below, we show an example test script \verb|test_Biospam.py|. If you put this script in the Biopython \verb|Tests| directory, then \verb|run_tests.py| will find it and execute the tests contained in it:
-
-\begin{minted}{console}
-$ python run_tests.py
-test_Ace ... ok
-test_AlignIO ... ok
-test_BioSQL ... ok
-test_BioSQL_SeqIO ... ok
-test_Biospam ... ok
-test_CAPS ... ok
-test_Clustalw ... ok
-...
-----------------------------------------------------------------------
-Ran 107 tests in 86.127 seconds
-\end{minted}
-
-\subsection{Writing a test using \texttt{unittest}}
-
-The \verb|unittest|-framework has been included with Python since version
-2.1, and is documented in the Python Library Reference (which I know you
-are keeping under your pillow, as recommended).  There is also
-\href{https://docs.python.org/3/library/unittest.html}{online documentation
-for unittest}.
-If you are familiar with the \verb|unittest| system (or something similar
-like the nose test framework), you shouldn't have any trouble.  You may
-find looking at the existing examples within Biopython helpful too.
-
-Here's a minimal \verb|unittest|-style test script for \verb|Biospam|,
-which you can copy and paste to get started:
-
-\begin{minted}{python}
-import unittest
-from Bio import Biospam
-
-
-class BiospamTestAddition(unittest.TestCase):
-    def test_addition1(self):
-        result = Biospam.addition(2, 3)
-        self.assertEqual(result, 5)
-
-    def test_addition2(self):
-        result = Biospam.addition(9, -1)
-        self.assertEqual(result, 8)
-
-
-class BiospamTestDivision(unittest.TestCase):
-    def test_division1(self):
-        result = Biospam.division(3.0, 2.0)
-        self.assertAlmostEqual(result, 1.5)
-
-    def test_division2(self):
-        result = Biospam.division(10.0, -2.0)
-        self.assertAlmostEqual(result, -5.0)
-
-
-if __name__ == "__main__":
-    runner = unittest.TextTestRunner(verbosity=2)
-    unittest.main(testRunner=runner)
-\end{minted}
-
-In the division tests, we use \verb|assertAlmostEqual| instead of \verb|assertEqual| to avoid tests failing due to roundoff errors; see the \verb|unittest| chapter in the Python documentation for details and for other functionality available in \verb|unittest| (\href{https://docs.python.org/3/library/unittest.html}{online reference}).
-
-These are the key points of \verb|unittest|-based tests:
-
-\begin{itemize}
-  \item Test cases are stored in classes that derive from
-    \verb|unittest.TestCase| and cover one basic aspect of your code
-
-  \item You can use methods \verb|setUp| and \verb|tearDown| for any repeated
-    code which should be run before and after each test method.  For example,
-    the \verb|setUp| method might be used to create an instance of the object
-    you are testing, or open a file handle.  The \verb|tearDown| should do any
-    ``tidying up'', for example closing the file handle.
-
-  \item The tests are prefixed with \verb|test_| and each test should cover
-    one specific part of what you are trying to test. You can have as
-    many tests as you want in a class.
-
-  \item At the end of the test script, you can use
-\begin{minted}{python}
-if __name__ == "__main__":
-    runner = unittest.TextTestRunner(verbosity=2)
-    unittest.main(testRunner=runner)
-\end{minted}
-        to execute the tests when the script is run by itself (rather than
-        imported from \verb|run_tests.py|).
-        If you run this script, then you'll see something like the following:
-
-\begin{minted}{console}
-$ python test_BiospamMyModule.py
-test_addition1 (__main__.TestAddition) ... ok
-test_addition2 (__main__.TestAddition) ... ok
-test_division1 (__main__.TestDivision) ... ok
-test_division2 (__main__.TestDivision) ... ok
-
-----------------------------------------------------------------------
-Ran 4 tests in 0.059s
-
-OK
-\end{minted}
-
-  \item To indicate more clearly what each test is doing, you can add
-        docstrings to each test.  These are shown when running the tests,
-        which can be useful information if a test is failing.
-
-\begin{minted}{python}
-import unittest
-from Bio import Biospam
-
-
-class BiospamTestAddition(unittest.TestCase):
-    def test_addition1(self):
-        """An addition test"""
-        result = Biospam.addition(2, 3)
-        self.assertEqual(result, 5)
-
-    def test_addition2(self):
-        """A second addition test"""
-        result = Biospam.addition(9, -1)
-        self.assertEqual(result, 8)
-
-
-class BiospamTestDivision(unittest.TestCase):
-    def test_division1(self):
-        """Now let's check division"""
-        result = Biospam.division(3.0, 2.0)
-        self.assertAlmostEqual(result, 1.5)
-
-    def test_division2(self):
-        """A second division test"""
-        result = Biospam.division(10.0, -2.0)
-        self.assertAlmostEqual(result, -5.0)
-
-
-if __name__ == "__main__":
-    runner = unittest.TextTestRunner(verbosity=2)
-    unittest.main(testRunner=runner)
-\end{minted}
-
-        Running the script will now show you:
-
-\begin{minted}{console}
-$ python test_BiospamMyModule.py
-An addition test ... ok
-A second addition test ... ok
-Now let's check division ... ok
-A second division test ... ok
-
-----------------------------------------------------------------------
-Ran 4 tests in 0.001s
-
-OK
-\end{minted}
-\end{itemize}
-
-If your module contains docstring tests (see section \ref{sec:doctest}),
-you \emph{may} want to include those in the tests to be run. You can do so as
-follows by modifying the code under \verb|if __name__ == "__main__":|
-to look like this:
-
-\begin{minted}{python}
-if __name__ == "__main__":
-    unittest_suite = unittest.TestLoader().loadTestsFromName("test_Biospam")
-    doctest_suite = doctest.DocTestSuite(Biospam)
-    suite = unittest.TestSuite((unittest_suite, doctest_suite))
-    runner = unittest.TextTestRunner(sys.stdout, verbosity=2)
-    runner.run(suite)
-\end{minted}
-
-This is only relevant if you want to run the docstring tests when you
-execute \verb|python test_Biospam.py| if it has some complex run-time
-dependency checking.
-
-In general instead include the docstring tests by adding them to the
-\verb|run_tests.py| as explained below.
-
-\section{Writing doctests}
-\label{sec:doctest}
-
-Python modules, classes and functions support built in documentation using
-docstrings.  The \href{https://docs.python.org/3/library/doctest.html}{doctest
-framework} (included with Python) allows the developer to embed working
-examples in the docstrings, and have these examples automatically tested.
-
-Currently only part of Biopython includes doctests. The \verb|run_tests.py|
-script takes care of running the doctests. For this purpose, at the top of
-the \verb|run_tests.py| script is a manually compiled list of modules to
-skip, important where optional external dependencies which may
-not be installed (e.g. the Reportlab and NumPy libraries).  So, if you've
-added some doctests to the docstrings in a Biopython module, in order to
-have them excluded in the Biopython test suite, you must update
-\verb|run_tests.py| to include your module. Currently, the relevant part
-of \verb|run_tests.py| looks as follows:
-
-\begin{minted}{python}
-# Following modules have historic failures. If you fix one of these
-# please remove here!
-EXCLUDE_DOCTEST_MODULES = [
-    "Bio.PDB",
-    "Bio.PDB.AbstractPropertyMap",
-    "Bio.Phylo.Applications._Fasttree",
-    "Bio.Phylo._io",
-    "Bio.Phylo.TreeConstruction",
-    "Bio.Phylo._utils",
-]
-
-# Exclude modules with online activity
-# They are not excluded by default, use --offline to exclude them
-ONLINE_DOCTEST_MODULES = ["Bio.Entrez", "Bio.ExPASy", "Bio.TogoWS"]
-
-# Silently ignore any doctests for modules requiring numpy!
-if numpy is None:
-    EXCLUDE_DOCTEST_MODULES.extend(
-        [
-            "Bio.Affy.CelFile",
-            "Bio.Cluster",
-            # ...
-        ]
-    )
-\end{minted}
-
-Note that we regard doctests primarily as documentation, so you should
-stick to typical usage. Generally complicated examples dealing with error
-conditions and the like would be best left to a dedicated unit test.
-
-Note that if you want to write doctests involving file parsing, defining
-the file location complicates matters.  Ideally use relative paths assuming
-the code will be run from the \verb|Tests| directory, see the
-\verb|Bio.SeqIO| doctests for an example of this.
-
-To run the docstring tests only, use
-\begin{minted}{console}
-$ python run_tests.py doctest
-\end{minted}
-
-Note that the doctest system is fragile and care is needed to ensure
-your output will match on all the different versions of Python that
-Biopython supports (e.g. differences in floating point numbers).
-
-\section{Writing doctests in the Tutorial}
-\label{sec:doctest-tutorial}
-
-This Tutorial you are reading has a lot of code snippets, which are
-often formatted like a doctest. We have our own system in file
-\verb|test_Tutorial.py| to allow tagging code snippets in the
-Tutorial source to be run as Python doctests. This works by adding
-special \verb|%doctest| comment lines before each Python block,
-e.g.
-
-\begin{verbatim}
-
-%doctest
-\begin{minted}{pycon}
->>> from Bio.Seq import Seq
->>> s = Seq("ACGT")
->>> len(s)
-4
-\end{minted}
-\end{verbatim}
-
-\noindent Often code examples are not self-contained, but
-continue from the previous Python block. Here we use the
-magic comment \verb|%cont-doctest| as shown here:
-
-\begin{verbatim}
-
-%cont-doctest
-\begin{minted}{pycon}
->>> s == "ACGT"
-True
-\end{minted}
-\end{verbatim}
-
-The special \verb|%doctest| comment line can take a working directory
-(relative to the \verb|Doc/| folder) to use if you have any example
-data files, e.g. \verb|%doctest examples| will use the
-\verb|Doc/examples| folder, while  \verb|%doctest ../Tests/GenBank|
-will use the \verb|Tests/GenBank| folder.
-
-After the directory argument, you can specify any Python dependencies
-which must be present in order to run the test by adding \verb|lib:XXX|
-to indicate \verb|import XXX| must work, e.g.
-\verb|%doctest examples lib:numpy|
-
-You can run the Tutorial doctests via:
-
-\begin{minted}{console}
-$ python test_Tutorial.py
-\end{minted}
-
-or:
-
-\begin{minted}{console}
-$ python run_tests.py test_Tutorial.py
-\end{minted}
diff --git a/Tests/test_Tutorial.py b/Tests/test_Tutorial.py
--- a/Tests/test_Tutorial.py
+++ b/Tests/test_Tutorial.py
@@ -1,34 +1,38 @@
-# Copyright 2011-2016 by Peter Cock.  All rights reserved.
+# Copyright 2011-2023 by Peter Cock.  All rights reserved.
+# Revisions copyright 2019 by Anil Tuncel. All rights reserved.
+#
 # This code is part of the Biopython distribution and governed by its
 # license.  Please see the LICENSE file that should have been included
 # as part of this package.
 #
-# This script looks for entries in the LaTeX source for the
+# This script looks for entries in the RST source for the
 # Biopython Tutorial which can be turned into Python doctests,
 # e.g.
 #
-# %doctest
-# \begin{minted}{pycon}
-# >>> from Bio.Seq import Seq
-# >>> s = Seq("ACGT")
-# >>> len(s)
-# 4
-# \end{minted}
+# .. doctest
+#
+# .. code:: pycon
+#
+#    >>> from Bio.Seq import Seq
+#    >>> s = Seq("ACGT")
+#    >>> len(s)
+#    4
 #
 # Code snippets can be extended using a similar syntax, which
 # will create a single combined doctest:
 #
-# %cont-doctest
-# \begin{minted}{pycon}
-# >>> s == "ACGT"
-# True
-# \end{minted}
+# .. cont-doctest
+#
+# .. code:: pycon
+#
+#   >>> s == "ACGT"
+#   True
 #
-# The %doctest line also supports a relative working directory,
+# The doctest comment line also supports a relative working directory,
 # and listing multiple Python dependencies as lib:XXX which will
 # ensure "import XXX" works before using the test. e.g.
 #
-# %doctest examples lib:numpy lib:scipy
+# .. doctest examples lib:numpy lib:scipy
 #
 # Additionally after the path, special keyword 'internet' is
 # used to flag online tests.
@@ -37,9 +41,9 @@
 # include a relative path to the working directory, just use '.'
 # for the default path, e.g.
 #
-# %doctest . lib:reportlab
+# .. doctest . lib:reportlab
 #
-# %doctest . internet
+# .. doctest . internet
 #
 # TODO: Adding bin:XXX for checking binary XXX is on $PATH?
 #
@@ -47,7 +51,6 @@
 # itself.
 
 
-# This future import will apply to all the doctests too:
 """Tests for Tutorial module."""
 
 import unittest
@@ -83,77 +86,96 @@
     tutorial_base = os.path.abspath(
         os.path.join(os.path.dirname(sys.argv[0]), "../Doc/")
     )
-    tutorial = os.path.join(tutorial_base, "Tutorial.tex")
+    tutorial = os.path.join(tutorial_base, "Tutorial/index.rst")
 else:
     # Probably called via run_tests.py so current directory should (now) be Tests/
     # but may have been changed by run_tests.py so can't infer from sys.argv[0] with e.g.
     # [base]$ python Tests/run_tests.py test_Tutorial
     tutorial_base = os.path.abspath("../Doc/")
-    tutorial = os.path.join(tutorial_base, "Tutorial.tex")
+    tutorial = os.path.join(tutorial_base, "Tutorial/index.rst")
 if not os.path.isfile(tutorial):
     from Bio import MissingExternalDependencyError
 
-    raise MissingExternalDependencyError("Could not find ../Doc/Tutorial.tex file")
+    raise MissingExternalDependencyError(
+        "Could not find ../Doc/Tutorial/index.rst file"
+    )
 
-# Build a list of all the Tutorial LaTeX files:
-files = [tutorial]
-for latex in os.listdir(os.path.join(tutorial_base, "Tutorial/")):
-    if latex.startswith("chapter_") and latex.endswith(".tex"):
-        files.append(os.path.join(tutorial_base, "Tutorial", latex))
+# Build a list of all the Tutorial RST files:
+files = []
+for rst in os.listdir(os.path.join(tutorial_base, "Tutorial/")):
+    if rst.startswith("chapter_") and rst.endswith(".rst"):
+        files.append(os.path.join(tutorial_base, "Tutorial", rst))
 
 
 def _extract(handle):
     line = handle.readline()
-    if line != "\\begin{minted}{pycon}\n":
-        if not (line.startswith("\\begin{minted}[") and line.endswith("]{pycon}\n")):
-            raise ValueError(
-                "Any '%doctest' or '%cont-doctest' line should be followed by '\\begin{minted}{pycon}' or '\\begin{minted}[options]{pycon}'"
-            )
+    if line != "\n":
+        raise ValueError(
+            "Any '.. doctest' or '.. cont-doctest' line should "
+            "be followed by an empty line"
+        )
+
+    line = handle.readline()
+    if line.lstrip() != ".. code:: pycon\n":
+        raise ValueError(
+            "Any '.. doctest' or '.. cont-doctest' line should "
+            r"be followed by '\n.. code:: pycon\n\n"
+        )
+
+    line = handle.readline()
+    if line != "\n":
+        raise ValueError(
+            "Any '.. doctest' or '.. cont-doctest' line should "
+            r"be followed by '\n.. code:: pycon\n\n"
+        )
+
     lines = []
     while True:
         line = handle.readline()
         if not line:
             if lines:
-                print("".join(lines[:30]))
-                raise ValueError("Didn't find end of test starting: %r", lines[0])
+                # print("".join(lines[:30]))
+                break
             else:
-                raise ValueError("Didn't find end of test!")
-        elif line.startswith("\\end{minted}"):
+                raise ValueError("Didn't find lines!")
+        elif line == "\n":
             break
         else:
             lines.append(line)
     return lines
 
 
-def extract_doctests(latex_filename):
-    """Scan LaTeX file and pull out marked doctests as strings.
+def extract_doctests(rst_filename):
+    """Scan RST file and pull out marked doctests as strings.
 
     This is a generator, yielding one tuple per doctest.
     """
-    base_name = os.path.splitext(os.path.basename(latex_filename))[0]
+    base_name = os.path.splitext(os.path.basename(rst_filename))[0]
+    name = None
     deps = ""
     folder = ""
-    with open(latex_filename) as handle:
+    with open(rst_filename, encoding="utf8") as handle:
         line_number = 0
         lines = []
-        name = None
         while True:
             line = handle.readline()
             line_number += 1
             if not line:
                 # End of file
                 break
-            elif line.startswith("%cont-doctest"):
+            elif line.lstrip().startswith(".. cont-doctest"):
                 x = _extract(handle)
                 lines.extend(x)
                 line_number += len(x) + 2
-            elif line.startswith("%doctest"):
+            elif line.lstrip().startswith(".. doctest"):
                 if lines:
-                    if not lines[0].startswith(">>> "):
-                        raise ValueError(f"Should start '>>> ' not {lines[0]!r}")
+                    if not lines[0].lstrip().startswith(">>> "):
+                        raise ValueError(
+                            f"Should start with '>>> ' (indented), not {lines[0]!r}"
+                        )
                     yield name, "".join(lines), folder, deps
                     lines = []
-                deps = [x.strip() for x in line.split()[1:]]
+                deps = [x.strip() for x in line.split()[2:]]
                 if deps:
                     folder = deps[0]
                     deps = deps[1:]
@@ -164,7 +186,9 @@ def extract_doctests(latex_filename):
                 lines.extend(x)
                 line_number += len(x) + 2
     if lines:
-        if not lines[0].startswith(">>> "):
+        if not name:
+            raise ValueError(f"Unanchored doctest in {rst_filename}: {lines}")
+        if not lines[0].lstrip().startswith(">>> "):
             raise ValueError(f"Should start '>>> ' not {lines[0]!r}")
         yield name, "".join(lines), folder, deps
     # yield "dummy", ">>> 2 + 2\n5\n"
@@ -194,9 +218,10 @@ def check_deps(dependencies):
 
 # Create dummy methods on the object purely to hold doctests
 missing_deps = set()
-for latex in files:
-    # print("Extracting doctests from %s" % latex)
-    for name, example, folder, deps in extract_doctests(latex):
+for rst in files:
+    # print("Extracting doctests from %s" % rst)
+    for name, example, folder, deps in extract_doctests(rst):
+        assert name, rst
         missing = check_deps(deps)
         if missing:
             missing_deps.update(missing)