Hello community,

here is the log from the commit of package python-certbot for openSUSE:Factory 
checked in at 2018-12-18 14:57:47
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-certbot (Old)
 and      /work/SRC/openSUSE:Factory/.python-certbot.new.28833 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-certbot"

Tue Dec 18 14:57:47 2018 rev:6 rq:658302 version:0.29.1

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-certbot/python-certbot.changes    
2018-12-06 12:19:18.861391188 +0100
+++ /work/SRC/openSUSE:Factory/.python-certbot.new.28833/python-certbot.changes 
2018-12-18 14:58:15.242263860 +0100
@@ -1,0 +2,18 @@
+Sat Dec 15 06:34:38 UTC 2018 - Thomas Bechtold <[email protected]>
+
+- update to 0.29.1:
+  * The default work and log directories have been changed back
+    to /var/lib/letsencrypt and /var/log/letsencrypt respectively.
+  * Noninteractive renewals with `certbot renew` (those not started
+    from a terminal) now randomly sleep 1-480 seconds before beginning
+    work in order to spread out load spikes on the server side.
+  * Added External Account Binding support in cli and acme library.
+    Command line arguments --eab-kid and --eab-hmac-key added.
+  * Private key permissioning changes: Renewal preserves existing group mode
+    & gid of previous private key material. Private keys for new
+    lineages (i.e. new certs, not renewed) default to 0o600.
+  * Update code and dependencies to clean up Resource and Deprecation Warnings.
+  * Only depend on imgconverter extension for Sphinx >= 1.6
+- update URL
+
+-------------------------------------------------------------------

Old:
----
  certbot-0.28.0.tar.gz

New:
----
  certbot-0.29.1.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-certbot.spec ++++++
--- /var/tmp/diff_new_pack.p2tm2z/_old  2018-12-18 14:58:15.930262820 +0100
+++ /var/tmp/diff_new_pack.p2tm2z/_new  2018-12-18 14:58:15.934262814 +0100
@@ -18,12 +18,12 @@
 
 %{?!python_module:%define python_module() python-%{**} python3-%{**}}
 Name:           python-certbot
-Version:        0.28.0
+Version:        0.29.1
 Release:        0
 Summary:        ACME client
 License:        Apache-2.0
 Group:          Development/Languages/Python
-URL:            https://github.com/letsencrypt/letsencrypt
+URL:            https://github.com/certbot/certbot
 Source:         
https://files.pythonhosted.org/packages/source/c/certbot/certbot-%{version}.tar.gz
 BuildRequires:  %{python_module acme >= 0.26.0}
 BuildRequires:  %{python_module configargparse >= 0.9.3}
@@ -42,7 +42,7 @@
 BuildRequires:  fdupes
 BuildRequires:  python-rpm-macros
 BuildRequires:  python2-typing
-Requires:       python-acme >= 0.26.0
+Requires:       python-acme >= 0.29.0
 Requires:       python-configargparse >= 0.9.3
 Requires:       python-configobj
 Requires:       python-cryptography >= 1.2

++++++ certbot-0.28.0.tar.gz -> certbot-0.29.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-0.28.0/CHANGELOG.md 
new/certbot-0.29.1/CHANGELOG.md
--- old/certbot-0.28.0/CHANGELOG.md     2018-11-07 22:14:56.000000000 +0100
+++ new/certbot-0.29.1/CHANGELOG.md     2018-12-06 00:47:58.000000000 +0100
@@ -2,6 +2,65 @@
 
 Certbot adheres to [Semantic Versioning](http://semver.org/).
 
+## 0.29.1 - 2018-12-05
+
+### Added
+
+*
+
+### Changed
+
+*
+
+### Fixed
+
+* The default work and log directories have been changed back to
+  /var/lib/letsencrypt and /var/log/letsencrypt respectively.
+
+Despite us having broken lockstep, we are continuing to release new versions of
+all Certbot components during releases for the time being, however, the only
+package with changes other than its version number was:
+
+* certbot
+
+More details about these changes can be found on our GitHub repo.
+
+## 0.29.0 - 2018-12-05
+
+### Added
+
+* Noninteractive renewals with `certbot renew` (those not started from a
+  terminal) now randomly sleep 1-480 seconds before beginning work in
+  order to spread out load spikes on the server side.
+* Added External Account Binding support in cli and acme library.
+  Command line arguments --eab-kid and --eab-hmac-key added.
+
+### Changed
+
+* Private key permissioning changes: Renewal preserves existing group mode
+  & gid of previous private key material. Private keys for new
+  lineages (i.e. new certs, not renewed) default to 0o600.
+
+### Fixed
+
+* Update code and dependencies to clean up Resource and Deprecation Warnings.
+* Only depend on imgconverter extension for Sphinx >= 1.6
+
+Despite us having broken lockstep, we are continuing to release new versions of
+all Certbot components during releases for the time being, however, the only
+package with changes other than its version number was:
+
+* acme
+* certbot
+* certbot-apache
+* certbot-dns-cloudflare
+* certbot-dns-digitalocean
+* certbot-dns-google
+* certbot-nginx
+
+More details about these changes can be found on our GitHub repo:
+https://github.com/certbot/certbot/milestone/62?closed=1
+
 ## 0.28.0 - 2018-11-7
 
 ### Added
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-0.28.0/PKG-INFO new/certbot-0.29.1/PKG-INFO
--- old/certbot-0.28.0/PKG-INFO 2018-11-07 22:14:58.000000000 +0100
+++ new/certbot-0.29.1/PKG-INFO 2018-12-06 00:47:59.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: certbot
-Version: 0.28.0
+Version: 0.29.1
 Summary: ACME client
 Home-page: https://github.com/letsencrypt/letsencrypt
 Author: Certbot Project
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-0.28.0/certbot/__init__.py 
new/certbot-0.29.1/certbot/__init__.py
--- old/certbot-0.28.0/certbot/__init__.py      2018-11-07 22:14:57.000000000 
+0100
+++ new/certbot-0.29.1/certbot/__init__.py      2018-12-06 00:47:59.000000000 
+0100
@@ -1,4 +1,4 @@
 """Certbot client."""
 
 # version number like 1.2.3a0, must have at least 2 parts, like 1.2
-__version__ = '0.28.0'
+__version__ = '0.29.1'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-0.28.0/certbot/cli.py 
new/certbot-0.29.1/certbot/cli.py
--- old/certbot-0.28.0/certbot/cli.py   2018-11-07 22:14:56.000000000 +0100
+++ new/certbot-0.29.1/certbot/cli.py   2018-12-06 00:47:58.000000000 +0100
@@ -172,10 +172,11 @@
         # need warnings
         return
     if "CERTBOT_AUTO" not in os.environ:
-        logger.warning("You are running with an old copy of letsencrypt-auto 
that does "
-            "not receive updates, and is less reliable than more recent 
versions. "
-            "We recommend upgrading to the latest certbot-auto script, or 
using native "
-            "OS packages.")
+        logger.warning("You are running with an old copy of letsencrypt-auto"
+            " that does not receive updates, and is less reliable than more"
+            " recent versions. The letsencrypt client has also been renamed"
+            " to Certbot. We recommend upgrading to the latest certbot-auto"
+            " script, or using native OS packages.")
         logger.debug("Deprecation warning circumstances: %s / %s", 
sys.argv[0], os.environ)
 
 
@@ -286,7 +287,9 @@
     """
     try:
         filename = os.path.abspath(filename)
-        return filename, open(filename, mode).read()
+        with open(filename, mode) as the_file:
+            contents = the_file.read()
+        return filename, contents
     except IOError as exc:
         raise argparse.ArgumentTypeError(exc.strerror)
 
@@ -856,7 +859,9 @@
         if chosen_topic == "everything":
             chosen_topic = "run"
         if chosen_topic == "all":
-            return dict([(t, True) for t in self.help_topics])
+            # Addition of condition closes #6209 (removal of duplicate route53 
option).
+            return dict([(t, True) if t != 'certbot-route53:auth' else (t, 
False)
+                         for t in self.help_topics])
         elif not chosen_topic:
             return dict([(t, False) for t in self.help_topics])
         else:
@@ -942,6 +947,18 @@
              "name. In the case of a name collision it will append a number "
              "like 0001 to the file path name. (default: Ask)")
     helpful.add(
+        [None, "run", "certonly", "register"],
+        "--eab-kid", dest="eab_kid",
+        metavar="EAB_KID",
+        help="Key Identifier for External Account Binding"
+    )
+    helpful.add(
+        [None, "run", "certonly", "register"],
+        "--eab-hmac-key", dest="eab_hmac_key",
+        metavar="EAB_HMAC_KEY",
+        help="HMAC key for External Account Binding"
+    )
+    helpful.add(
         [None, "run", "certonly", "manage", "delete", "certificates",
          "renew", "enhance"], "--cert-name", dest="certname",
         metavar="CERTNAME", default=flag_default("certname"),
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-0.28.0/certbot/client.py 
new/certbot-0.29.1/certbot/client.py
--- old/certbot-0.28.0/certbot/client.py        2018-11-07 22:14:56.000000000 
+0100
+++ new/certbot-0.29.1/certbot/client.py        2018-12-06 00:47:58.000000000 
+0100
@@ -203,9 +203,27 @@
     :returns: Registration Resource.
     :rtype: `acme.messages.RegistrationResource`
     """
+
+    eab_credentials_supplied = config.eab_kid and config.eab_hmac_key
+    if eab_credentials_supplied:
+        account_public_key = acme.client.net.key.public_key()
+        eab = 
messages.ExternalAccountBinding.from_data(account_public_key=account_public_key,
+                                                        kid=config.eab_kid,
+                                                        
hmac_key=config.eab_hmac_key,
+                                                        
directory=acme.client.directory)
+    else:
+        eab = None
+
+    if acme.external_account_required():
+        if not eab_credentials_supplied:
+            msg = ("Server requires external account binding."
+                   " Please use --eab-kid and --eab-hmac-key.")
+            raise errors.Error(msg)
+
     try:
-        return 
acme.new_account_and_tos(messages.NewRegistration.from_data(email=config.email),
-            tos_cb)
+        newreg = messages.NewRegistration.from_data(email=config.email,
+                                                    
external_account_binding=eab)
+        return acme.new_account_and_tos(newreg, tos_cb)
     except messages.Error as e:
         if e.code == "invalidEmail" or e.code == "invalidContact":
             if config.noninteractive_mode:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-0.28.0/certbot/compat.py 
new/certbot-0.29.1/certbot/compat.py
--- old/certbot-0.28.0/certbot/compat.py        2018-11-07 22:14:56.000000000 
+0100
+++ new/certbot-0.29.1/certbot/compat.py        2018-12-06 00:47:58.000000000 
+0100
@@ -172,3 +172,30 @@
     # Windows specific: most of mode bits are ignored on Windows. Only check 
user R/W rights.
     return (stat.S_IMODE(mode1) & stat.S_IREAD == stat.S_IMODE(mode2) & 
stat.S_IREAD
             and stat.S_IMODE(mode1) & stat.S_IWRITE == stat.S_IMODE(mode2) & 
stat.S_IWRITE)
+
+WINDOWS_DEFAULT_FOLDERS = {
+    'config': 'C:\\Certbot',
+    'work': 'C:\\Certbot\\lib',
+    'logs': 'C:\\Certbot\\log',
+}
+LINUX_DEFAULT_FOLDERS = {
+    'config': '/etc/letsencrypt',
+    'work': '/var/lib/letsencrypt',
+    'logs': '/var/log/letsencrypt',
+}
+
+def get_default_folder(folder_type):
+    """
+    Return the relevant default folder for the current OS
+
+    :param str folder_type: The type of folder to retrieve (config, work or 
logs)
+
+    :returns: The relevant default folder.
+    :rtype: str
+
+    """
+    if 'fcntl' in sys.modules:
+        # Linux specific
+        return LINUX_DEFAULT_FOLDERS[folder_type]
+    # Windows specific
+    return WINDOWS_DEFAULT_FOLDERS[folder_type]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-0.28.0/certbot/constants.py 
new/certbot-0.29.1/certbot/constants.py
--- old/certbot-0.28.0/certbot/constants.py     2018-11-07 22:14:56.000000000 
+0100
+++ new/certbot-0.29.1/certbot/constants.py     2018-12-06 00:47:58.000000000 
+0100
@@ -4,7 +4,7 @@
 import pkg_resources
 
 from acme import challenges
-
+from certbot import compat
 
 SETUPTOOLS_PLUGINS_ENTRY_POINT = "certbot.plugins"
 """Setuptools entry point group name for plugins."""
@@ -14,7 +14,7 @@
 
 CLI_DEFAULTS = dict(
     config_files=[
-        "/etc/letsencrypt/cli.ini",
+        os.path.join(compat.get_default_folder('config'), 'cli.ini'),
         # http://freedesktop.org/wiki/Software/xdg-user-dirs/
         os.path.join(os.environ.get("XDG_CONFIG_HOME", "~/.config"),
                      "letsencrypt", "cli.ini"),
@@ -68,6 +68,8 @@
     directory_hooks=True,
     reuse_key=False,
     disable_renew_updates=False,
+    eab_hmac_key=None,
+    eab_kid=None,
 
     # Subparsers
     num=None,
@@ -85,9 +87,9 @@
     auth_cert_path="./cert.pem",
     auth_chain_path="./chain.pem",
     key_path=None,
-    config_dir="/etc/letsencrypt",
-    work_dir="/var/lib/letsencrypt",
-    logs_dir="/var/log/letsencrypt",
+    config_dir=compat.get_default_folder('config'),
+    work_dir=compat.get_default_folder('work'),
+    logs_dir=compat.get_default_folder('logs'),
     server="https://acme-v02.api.letsencrypt.org/directory";,
 
     # Plugins parsers
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-0.28.0/certbot/crypto_util.py 
new/certbot-0.29.1/certbot/crypto_util.py
--- old/certbot-0.28.0/certbot/crypto_util.py   2018-11-07 22:14:56.000000000 
+0100
+++ new/certbot-0.29.1/certbot/crypto_util.py   2018-12-06 00:47:58.000000000 
+0100
@@ -458,7 +458,7 @@
     :rtype: str
     """
     sha256 = hashlib.sha256()
-    with open(filename, 'rU') as file_d:
+    with open(filename, 'r') as file_d:
         sha256.update(file_d.read().encode('UTF-8'))
     return sha256.hexdigest()
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-0.28.0/certbot/display/ops.py 
new/certbot-0.29.1/certbot/display/ops.py
--- old/certbot-0.28.0/certbot/display/ops.py   2018-11-07 22:14:56.000000000 
+0100
+++ new/certbot-0.29.1/certbot/display/ops.py   2018-12-06 00:47:58.000000000 
+0100
@@ -4,9 +4,11 @@
 
 import zope.component
 
+from certbot import compat
 from certbot import errors
 from certbot import interfaces
 from certbot import util
+
 from certbot.display import util as display_util
 
 logger = logging.getLogger(__name__)
@@ -33,7 +35,8 @@
     unsafe_suggestion = ("\n\nIf you really want to skip this, you can run "
                          "the client with --register-unsafely-without-email "
                          "but make sure you then backup your account key from "
-                         "/etc/letsencrypt/accounts\n\n")
+                         "{0}\n\n".format(os.path.join(
+                             compat.get_default_folder('config'), 'accounts')))
     if optional:
         if invalid:
             msg += unsafe_suggestion
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-0.28.0/certbot/main.py 
new/certbot-0.29.1/certbot/main.py
--- old/certbot-0.28.0/certbot/main.py  2018-11-07 22:14:56.000000000 +0100
+++ new/certbot-0.29.1/certbot/main.py  2018-12-06 00:47:58.000000000 +0100
@@ -4,7 +4,9 @@
 import functools
 import logging.handlers
 import os
+import random
 import sys
+import time
 
 import configobj
 import josepy as jose
@@ -1243,6 +1245,16 @@
     :rtype: None
 
     """
+    if not sys.stdin.isatty():
+        # Noninteractive renewals include a random delay in order to spread
+        # out the load on the certificate authority servers, even if many
+        # users all pick the same time for renewals.  This delay precedes
+        # running any hooks, so that side effects of the hooks (such as
+        # shutting down a web service) aren't prolonged unnecessarily.
+        sleep_time = random.randint(1, 60*8)
+        logger.info("Non-interactive renewal: random delay of %s seconds", 
sleep_time)
+        time.sleep(sleep_time)
+
     try:
         renewal.handle_renewal_request(config)
     finally:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-0.28.0/certbot/ocsp.py 
new/certbot-0.29.1/certbot/ocsp.py
--- old/certbot-0.28.0/certbot/ocsp.py  2018-11-07 22:14:56.000000000 +0100
+++ new/certbot-0.29.1/certbot/ocsp.py  2018-12-06 00:47:58.000000000 +0100
@@ -114,7 +114,7 @@
             logger.info("OCSP revocation warning: %s", warning)
         return True
     else:
-        logger.warn("Unable to properly parse OCSP output: %s\nstderr:%s",
+        logger.warning("Unable to properly parse OCSP output: %s\nstderr:%s",
                     ocsp_output, ocsp_errors)
         return False
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-0.28.0/certbot/plugins/enhancements_test.py 
new/certbot-0.29.1/certbot/plugins/enhancements_test.py
--- old/certbot-0.28.0/certbot/plugins/enhancements_test.py     2018-11-07 
22:14:56.000000000 +0100
+++ new/certbot-0.29.1/certbot/plugins/enhancements_test.py     2018-12-06 
00:47:58.000000000 +0100
@@ -37,11 +37,11 @@
         self.assertTrue([i for i in enabled if i["name"] == "somethingelse"])
 
     def test_are_requested(self):
-        self.assertEquals(
+        self.assertEqual(
             len([i for i in enhancements.enabled_enhancements(self.config)]), 
0)
         self.assertFalse(enhancements.are_requested(self.config))
         self.config.auto_hsts = True
-        self.assertEquals(
+        self.assertEqual(
             len([i for i in enhancements.enabled_enhancements(self.config)]), 
1)
         self.assertTrue(enhancements.are_requested(self.config))
 
@@ -57,7 +57,7 @@
         lineage = "lineage"
         enhancements.enable(lineage, domains, self.mockinstaller, self.config)
         self.assertTrue(self.mockinstaller.enable_autohsts.called)
-        self.assertEquals(self.mockinstaller.enable_autohsts.call_args[0],
+        self.assertEqual(self.mockinstaller.enable_autohsts.call_args[0],
                           (lineage, domains))
 
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-0.28.0/certbot/plugins/selection_test.py 
new/certbot-0.29.1/certbot/plugins/selection_test.py
--- old/certbot-0.28.0/certbot/plugins/selection_test.py        2018-11-07 
22:14:56.000000000 +0100
+++ new/certbot-0.29.1/certbot/plugins/selection_test.py        2018-12-06 
00:47:58.000000000 +0100
@@ -197,7 +197,7 @@
 
     def test_no_installer_defined(self):
         self.config.configurator = None
-        self.assertEquals(self._call(), None)
+        self.assertEqual(self._call(), None)
 
     def test_no_available_installers(self):
         self.config.configurator = "apache"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-0.28.0/certbot/plugins/standalone_test.py 
new/certbot-0.29.1/certbot/plugins/standalone_test.py
--- old/certbot-0.28.0/certbot/plugins/standalone_test.py       2018-11-07 
22:14:56.000000000 +0100
+++ new/certbot-0.29.1/certbot/plugins/standalone_test.py       2018-12-06 
00:47:58.000000000 +0100
@@ -72,6 +72,8 @@
             errors.StandaloneBindError, self.mgr.run, port,
             challenge_type=challenges.HTTP01)
         self.assertEqual(self.mgr.running(), {})
+        some_server.close()
+        maybe_another_server.close()
 
 
 class SupportedChallengesActionTest(unittest.TestCase):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-0.28.0/certbot/renewal.py 
new/certbot-0.29.1/certbot/renewal.py
--- old/certbot-0.28.0/certbot/renewal.py       2018-11-07 22:14:56.000000000 
+0100
+++ new/certbot-0.29.1/certbot/renewal.py       2018-12-06 00:47:58.000000000 
+0100
@@ -276,8 +276,10 @@
     "Do not renew a valid cert with one from a staging server!"
     # Some lineages may have begun with --staging, but then had production 
certs
     # added to them
+    with open(lineage.cert) as the_file:
+        contents = the_file.read()
     latest_cert = OpenSSL.crypto.load_certificate(
-        OpenSSL.crypto.FILETYPE_PEM, open(lineage.cert).read())
+        OpenSSL.crypto.FILETYPE_PEM, contents)
     # all our test certs are from happy hacker fake CA, though maybe one day
     # we should test more methodically
     now_valid = "fake" not in repr(latest_cert.get_issuer()).lower()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-0.28.0/certbot/storage.py 
new/certbot-0.29.1/certbot/storage.py
--- old/certbot-0.28.0/certbot/storage.py       2018-11-07 22:14:56.000000000 
+0100
+++ new/certbot-0.29.1/certbot/storage.py       2018-12-06 00:47:58.000000000 
+0100
@@ -29,6 +29,7 @@
 ALL_FOUR = ("cert", "privkey", "chain", "fullchain")
 README = "README"
 CURRENT_VERSION = util.get_strict_version(certbot.__version__)
+BASE_PRIVKEY_MODE = 0o600
 
 
 def renewal_conf_files(config):
@@ -792,7 +793,7 @@
         May need to recover from rare interrupted / crashed states."""
 
         if self.has_pending_deployment():
-            logger.warn("Found a new cert /archive/ that was not linked to in 
/live/; "
+            logger.warning("Found a new cert /archive/ that was not linked to 
in /live/; "
                         "fixing...")
             self.update_all_links_to(self.latest_common_version())
             return False
@@ -1035,9 +1036,11 @@
         archive = full_archive_path(None, cli_config, lineagename)
         live_dir = _full_live_path(cli_config, lineagename)
         if os.path.exists(archive):
+            config_file.close()
             raise errors.CertStorageError(
                 "archive directory exists for " + lineagename)
         if os.path.exists(live_dir):
+            config_file.close()
             raise errors.CertStorageError(
                 "live directory exists for " + lineagename)
         os.mkdir(archive)
@@ -1048,13 +1051,14 @@
         # Put the data into the appropriate files on disk
         target = dict([(kind, os.path.join(live_dir, kind + ".pem"))
                        for kind in ALL_FOUR])
+        archive_target = dict([(kind, os.path.join(archive, kind + "1.pem"))
+                               for kind in ALL_FOUR])
         for kind in ALL_FOUR:
-            os.symlink(os.path.join(_relpath_from_file(archive, target[kind]), 
kind + "1.pem"),
-                       target[kind])
+            os.symlink(_relpath_from_file(archive_target[kind], target[kind]), 
target[kind])
         with open(target["cert"], "wb") as f:
             logger.debug("Writing certificate to %s.", target["cert"])
             f.write(cert)
-        with open(target["privkey"], "wb") as f:
+        with util.safe_open(archive_target["privkey"], "wb", 
chmod=BASE_PRIVKEY_MODE) as f:
             logger.debug("Writing private key to %s.", target["privkey"])
             f.write(privkey)
             # XXX: Let's make sure to get the file permissions right here
@@ -1118,14 +1122,15 @@
               os.path.join(self.archive_dir, "{0}{1}.pem".format(kind, 
target_version)))
              for kind in ALL_FOUR])
 
+        old_privkey = os.path.join(
+            self.archive_dir, "privkey{0}.pem".format(prior_version))
+
         # Distinguish the cases where the privkey has changed and where it
         # has not changed (in the latter case, making an appropriate symlink
         # to an earlier privkey version)
         if new_privkey is None:
             # The behavior below keeps the prior key by creating a new
             # symlink to the old key or the target of the old key symlink.
-            old_privkey = os.path.join(
-                self.archive_dir, "privkey{0}.pem".format(prior_version))
             if os.path.islink(old_privkey):
                 old_privkey = os.readlink(old_privkey)
             else:
@@ -1133,9 +1138,16 @@
             logger.debug("Writing symlink to old private key, %s.", 
old_privkey)
             os.symlink(old_privkey, target["privkey"])
         else:
-            with open(target["privkey"], "wb") as f:
+            with util.safe_open(target["privkey"], "wb", 
chmod=BASE_PRIVKEY_MODE) as f:
                 logger.debug("Writing new private key to %s.", 
target["privkey"])
                 f.write(new_privkey)
+            # Preserve gid and (mode & 074) from previous privkey in this 
lineage.
+            old_mode = stat.S_IMODE(os.stat(old_privkey).st_mode) & \
+                (stat.S_IRGRP | stat.S_IWGRP | stat.S_IXGRP | \
+                 stat.S_IROTH)
+            mode = BASE_PRIVKEY_MODE | old_mode
+            os.chown(target["privkey"], -1, os.stat(old_privkey).st_gid)
+            os.chmod(target["privkey"], mode)
 
         # Save everything else
         with open(target["cert"], "wb") as f:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-0.28.0/certbot/tests/cert_manager_test.py 
new/certbot-0.29.1/certbot/tests/cert_manager_test.py
--- old/certbot-0.28.0/certbot/tests/cert_manager_test.py       2018-11-07 
22:14:56.000000000 +0100
+++ new/certbot-0.29.1/certbot/tests/cert_manager_test.py       2018-12-06 
00:47:58.000000000 +0100
@@ -39,9 +39,8 @@
         # We also create a file that isn't a renewal config in the same
         # location to test that logic that reads in all-and-only renewal
         # configs will ignore it and NOT attempt to parse it.
-        junk = open(os.path.join(self.config.renewal_configs_dir, 
"IGNORE.THIS"), "w")
-        junk.write("This file should be ignored!")
-        junk.close()
+        with open(os.path.join(self.config.renewal_configs_dir, 
"IGNORE.THIS"), "w") as junk:
+            junk.write("This file should be ignored!")
 
     def _set_up_config(self, domain, custom_archive):
         # TODO: maybe provide NamespaceConfig.make_dirs?
@@ -589,7 +588,7 @@
         from certbot import cert_manager
         prompt = "Which certificate would you"
         self.mock_get_utility().menu.return_value = (display_util.OK, 0)
-        self.assertEquals(
+        self.assertEqual(
             cert_manager.get_certnames(
                 self.config, "verb", allow_multiple=False), ['example.com'])
         self.assertTrue(
@@ -603,11 +602,11 @@
         from certbot import cert_manager
         prompt = "custom prompt"
         self.mock_get_utility().menu.return_value = (display_util.OK, 0)
-        self.assertEquals(
+        self.assertEqual(
             cert_manager.get_certnames(
                 self.config, "verb", allow_multiple=False, 
custom_prompt=prompt),
             ['example.com'])
-        self.assertEquals(self.mock_get_utility().menu.call_args[0][0],
+        self.assertEqual(self.mock_get_utility().menu.call_args[0][0],
                           prompt)
 
     @mock.patch('certbot.storage.renewal_conf_files')
@@ -631,7 +630,7 @@
         prompt = "Which certificate(s) would you"
         self.mock_get_utility().checklist.return_value = (display_util.OK,
                                                           ['example.com'])
-        self.assertEquals(
+        self.assertEqual(
             cert_manager.get_certnames(
                 self.config, "verb", allow_multiple=True), ['example.com'])
         self.assertTrue(
@@ -646,11 +645,11 @@
         prompt = "custom prompt"
         self.mock_get_utility().checklist.return_value = (display_util.OK,
                                                           ['example.com'])
-        self.assertEquals(
+        self.assertEqual(
             cert_manager.get_certnames(
                 self.config, "verb", allow_multiple=True, 
custom_prompt=prompt),
             ['example.com'])
-        self.assertEquals(
+        self.assertEqual(
             self.mock_get_utility().checklist.call_args[0][0],
             prompt)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-0.28.0/certbot/tests/cli_test.py 
new/certbot-0.29.1/certbot/tests/cli_test.py
--- old/certbot-0.28.0/certbot/tests/cli_test.py        2018-11-07 
22:14:56.000000000 +0100
+++ new/certbot-0.29.1/certbot/tests/cli_test.py        2018-12-06 
00:47:58.000000000 +0100
@@ -4,6 +4,7 @@
 import os
 import tempfile
 import copy
+import sys
 
 import mock
 import six
@@ -41,6 +42,15 @@
         self.assertEqual(contents, test_contents)
 
 
+class FlagDefaultTest(unittest.TestCase):
+    """Tests cli.flag_default"""
+
+    def test_linux_directories(self):
+        if 'fcntl' in sys.modules:
+            self.assertEqual(cli.flag_default('config_dir'), 
'/etc/letsencrypt')
+            self.assertEqual(cli.flag_default('work_dir'), 
'/var/lib/letsencrypt')
+            self.assertEqual(cli.flag_default('logs_dir'), 
'/var/log/letsencrypt')
+
 
 class ParseTest(unittest.TestCase):  # pylint: disable=too-many-public-methods
     '''Test the cli args entrypoint'''
@@ -431,6 +441,11 @@
         self.assertRaises(errors.Error, self.parse,
                           "--allow-subset-of-names -d *.example.org".split())
 
+    def test_route53_no_revert(self):
+        for help_flag in ['-h', '--help']:
+            for topic in ['all', 'plugins', 'dns-route53']:
+                self.assertFalse('certbot-route53:auth' in 
self._help_output([help_flag, topic]))
+
 
 class DefaultTest(unittest.TestCase):
     """Tests for certbot.cli._Default."""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-0.28.0/certbot/tests/client_test.py 
new/certbot-0.29.1/certbot/tests/client_test.py
--- old/certbot-0.28.0/certbot/tests/client_test.py     2018-11-07 
22:14:56.000000000 +0100
+++ new/certbot-0.29.1/certbot/tests/client_test.py     2018-12-06 
00:47:58.000000000 +0100
@@ -13,6 +13,8 @@
 
 import certbot.tests.util as test_util
 
+from josepy import interfaces
+
 KEY = test_util.load_vector("rsa512_key.pem")
 CSR_SAN = test_util.load_vector("csr-san_512.pem")
 
@@ -64,9 +66,28 @@
         tos_cb = mock.MagicMock()
         return register(self.config, self.account_storage, tos_cb)
 
+    @staticmethod
+    def _public_key_mock():
+        m = mock.Mock(__class__=interfaces.JSONDeSerializable)
+        m.to_partial_json.return_value = '{"a": 1}'
+        return m
+
+    @staticmethod
+    def _new_acct_dir_mock():
+        return "/acme/new-account"
+
+    @staticmethod
+    def _true_mock():
+        return True
+
+    @staticmethod
+    def _false_mock():
+        return False
+
     def test_no_tos(self):
         with 
mock.patch("certbot.client.acme_client.BackwardsCompatibleClientV2") as 
mock_client:
             mock_client.new_account_and_tos().terms_of_service = "http://tos";
+            mock_client().external_account_required.side_effect = 
self._false_mock
             with mock.patch("certbot.eff.handle_subscription") as mock_handle:
                 with mock.patch("certbot.account.report_new_account"):
                     mock_client().new_account_and_tos.side_effect = 
errors.Error
@@ -78,7 +99,8 @@
                     self.assertTrue(mock_handle.called)
 
     def test_it(self):
-        with 
mock.patch("certbot.client.acme_client.BackwardsCompatibleClientV2"):
+        with 
mock.patch("certbot.client.acme_client.BackwardsCompatibleClientV2") as 
mock_client:
+            mock_client().external_account_required.side_effect = 
self._false_mock
             with mock.patch("certbot.account.report_new_account"):
                 with mock.patch("certbot.eff.handle_subscription"):
                     self._call()
@@ -91,6 +113,7 @@
         msg = "DNS problem: NXDOMAIN looking up MX for example.com"
         mx_err = messages.Error.with_code('invalidContact', detail=msg)
         with 
mock.patch("certbot.client.acme_client.BackwardsCompatibleClientV2") as 
mock_client:
+            mock_client().external_account_required.side_effect = 
self._false_mock
             with mock.patch("certbot.eff.handle_subscription") as mock_handle:
                 mock_client().new_account_and_tos.side_effect = [mx_err, 
mock.MagicMock()]
                 self._call()
@@ -104,6 +127,7 @@
         msg = "DNS problem: NXDOMAIN looking up MX for example.com"
         mx_err = messages.Error.with_code('invalidContact', detail=msg)
         with 
mock.patch("certbot.client.acme_client.BackwardsCompatibleClientV2") as 
mock_client:
+            mock_client().external_account_required.side_effect = 
self._false_mock
             with mock.patch("certbot.eff.handle_subscription"):
                 mock_client().new_account_and_tos.side_effect = [mx_err, 
mock.MagicMock()]
                 self.assertRaises(errors.Error, self._call)
@@ -115,7 +139,8 @@
     @mock.patch("certbot.client.logger")
     def test_without_email(self, mock_logger):
         with mock.patch("certbot.eff.handle_subscription") as mock_handle:
-            with 
mock.patch("certbot.client.acme_client.BackwardsCompatibleClientV2"):
+            with 
mock.patch("certbot.client.acme_client.BackwardsCompatibleClientV2") as 
mock_clnt:
+                mock_clnt().external_account_required.side_effect = 
self._false_mock
                 with mock.patch("certbot.account.report_new_account"):
                     self.config.email = None
                     self.config.register_unsafely_without_email = True
@@ -129,6 +154,7 @@
     def test_dry_run_no_staging_account(self, _rep, mock_get_email):
         """Tests dry-run for no staging account, expect account created with 
no email"""
         with 
mock.patch("certbot.client.acme_client.BackwardsCompatibleClientV2") as 
mock_client:
+            mock_client().external_account_required.side_effect = 
self._false_mock
             with mock.patch("certbot.eff.handle_subscription"):
                 with mock.patch("certbot.account.report_new_account"):
                     self.config.dry_run = True
@@ -138,11 +164,53 @@
                     # check Certbot created an account with no email. Contact 
should return empty
                     
self.assertFalse(mock_client().new_account_and_tos.call_args[0][0].contact)
 
+    def test_with_eab_arguments(self):
+        with 
mock.patch("certbot.client.acme_client.BackwardsCompatibleClientV2") as 
mock_client:
+            mock_client().client.directory.__getitem__ = mock.Mock(
+                side_effect=self._new_acct_dir_mock
+            )
+            mock_client().external_account_required.side_effect = 
self._false_mock
+            with mock.patch("certbot.eff.handle_subscription"):
+                target = 
"certbot.client.messages.ExternalAccountBinding.from_data"
+                with mock.patch(target) as mock_eab_from_data:
+                    self.config.eab_kid = "test-kid"
+                    self.config.eab_hmac_key = 
"J2OAqW4MHXsrHVa_PVg0Y-L_R4SYw0_aL1le6mfblbE"
+                    self._call()
+
+                    self.assertTrue(mock_eab_from_data.called)
+
+    def test_without_eab_arguments(self):
+        with 
mock.patch("certbot.client.acme_client.BackwardsCompatibleClientV2") as 
mock_client:
+            mock_client().external_account_required.side_effect = 
self._false_mock
+            with mock.patch("certbot.eff.handle_subscription"):
+                target = 
"certbot.client.messages.ExternalAccountBinding.from_data"
+                with mock.patch(target) as mock_eab_from_data:
+                    self.config.eab_kid = None
+                    self.config.eab_hmac_key = None
+                    self._call()
+
+                    self.assertFalse(mock_eab_from_data.called)
+
+    def test_external_account_required_without_eab_arguments(self):
+        with 
mock.patch("certbot.client.acme_client.BackwardsCompatibleClientV2") as 
mock_client:
+            mock_client().client.net.key.public_key = 
mock.Mock(side_effect=self._public_key_mock)
+            mock_client().external_account_required.side_effect = 
self._true_mock
+            with mock.patch("certbot.eff.handle_subscription"):
+                with 
mock.patch("certbot.client.messages.ExternalAccountBinding.from_data"):
+                    self.config.eab_kid = None
+                    self.config.eab_hmac_key = None
+
+                    self.assertRaises(errors.Error, self._call)
+
     def test_unsupported_error(self):
         from acme import messages
         msg = "Test"
         mx_err = messages.Error(detail=msg, typ="malformed", title="title")
         with 
mock.patch("certbot.client.acme_client.BackwardsCompatibleClientV2") as 
mock_client:
+            mock_client().client.directory.__getitem__ = mock.Mock(
+                side_effect=self._new_acct_dir_mock
+            )
+            mock_client().external_account_required.side_effect = 
self._false_mock
             with mock.patch("certbot.eff.handle_subscription") as mock_handle:
                 mock_client().new_account_and_tos.side_effect = [mx_err, 
mock.MagicMock()]
                 self.assertRaises(messages.Error, self._call)
@@ -487,7 +555,7 @@
         self.config.hsts = True
         self._test_with_already_existing()
         self.assertTrue(mock_log.warning.called)
-        self.assertEquals(mock_log.warning.call_args[0][1],
+        self.assertEqual(mock_log.warning.call_args[0][1],
                           'Strict-Transport-Security')
 
     @mock.patch("certbot.client.logger")
@@ -495,7 +563,7 @@
         self.config.redirect = True
         self._test_with_already_existing()
         self.assertTrue(mock_log.warning.called)
-        self.assertEquals(mock_log.warning.call_args[0][1],
+        self.assertEqual(mock_log.warning.call_args[0][1],
                           'redirect')
 
     def test_no_ask_hsts(self):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-0.28.0/certbot/tests/display/ops_test.py 
new/certbot-0.29.1/certbot/tests/display/ops_test.py
--- old/certbot-0.28.0/certbot/tests/display/ops_test.py        2018-11-07 
22:14:56.000000000 +0100
+++ new/certbot-0.29.1/certbot/tests/display/ops_test.py        2018-12-06 
00:47:58.000000000 +0100
@@ -502,9 +502,9 @@
         items = ["first", "second", "third"]
         mock_util().checklist.return_value = (display_util.OK, [items[2]])
         result = self._call(items, None)
-        self.assertEquals(result, [items[2]])
+        self.assertEqual(result, [items[2]])
         self.assertTrue(mock_util().checklist.called)
-        self.assertEquals(mock_util().checklist.call_args[0][0], None)
+        self.assertEqual(mock_util().checklist.call_args[0][0], None)
 
     @test_util.patch_get_utility("certbot.display.ops.z_util")
     def test_choose_names_success_question(self, mock_util):
@@ -512,9 +512,9 @@
         question = "Which one?"
         mock_util().checklist.return_value = (display_util.OK, [items[1]])
         result = self._call(items, question)
-        self.assertEquals(result, [items[1]])
+        self.assertEqual(result, [items[1]])
         self.assertTrue(mock_util().checklist.called)
-        self.assertEquals(mock_util().checklist.call_args[0][0], question)
+        self.assertEqual(mock_util().checklist.call_args[0][0], question)
 
     @test_util.patch_get_utility("certbot.display.ops.z_util")
     def test_choose_names_user_cancel(self, mock_util):
@@ -522,9 +522,9 @@
         question = "Want to cancel?"
         mock_util().checklist.return_value = (display_util.CANCEL, [])
         result = self._call(items, question)
-        self.assertEquals(result, [])
+        self.assertEqual(result, [])
         self.assertTrue(mock_util().checklist.called)
-        self.assertEquals(mock_util().checklist.call_args[0][0], question)
+        self.assertEqual(mock_util().checklist.call_args[0][0], question)
 
 
 if __name__ == "__main__":
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-0.28.0/certbot/tests/display/util_test.py 
new/certbot-0.29.1/certbot/tests/display/util_test.py
--- old/certbot-0.28.0/certbot/tests/display/util_test.py       2018-11-07 
22:14:56.000000000 +0100
+++ new/certbot-0.29.1/certbot/tests/display/util_test.py       2018-12-06 
00:47:58.000000000 +0100
@@ -49,6 +49,7 @@
         stdin.listen(1)
         with mock.patch("certbot.display.util.sys.stdin", stdin):
             self.assertRaises(errors.Error, self._call, timeout=0.001)
+        stdin.close()
 
 
 class FileOutputDisplayTest(unittest.TestCase):
@@ -314,7 +315,11 @@
         # Every IDisplay method implemented by FileDisplay must take
         # force_interactive to prevent workflow regressions.
         for name in interfaces.IDisplay.names():  # pylint: disable=no-member
-            arg_spec = inspect.getargspec(getattr(self.displayer, name))
+            if six.PY2:
+                getargspec = inspect.getargspec # pylint: disable=no-member
+            else:
+                getargspec = inspect.getfullargspec # pylint: disable=no-member
+            arg_spec = getargspec(getattr(self.displayer, name))
             self.assertTrue("force_interactive" in arg_spec.args)
 
 
@@ -371,7 +376,12 @@
         for name in interfaces.IDisplay.names():  # pylint: disable=no-member
             method = getattr(self.displayer, name)
             # asserts method accepts arbitrary keyword arguments
-            self.assertFalse(inspect.getargspec(method).keywords is None)
+            if six.PY2:
+                result = inspect.getargspec(method).keywords # pylint: 
disable=no-member
+                self.assertFalse(result is None)
+            else:
+                result = inspect.getfullargspec(method).varkw # pylint: 
disable=no-member
+                self.assertFalse(result is None)
 
 
 class SeparateListInputTest(unittest.TestCase):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-0.28.0/certbot/tests/log_test.py 
new/certbot-0.29.1/certbot/tests/log_test.py
--- old/certbot-0.28.0/certbot/tests/log_test.py        2018-11-07 
22:14:56.000000000 +0100
+++ new/certbot-0.29.1/certbot/tests/log_test.py        2018-12-06 
00:47:58.000000000 +0100
@@ -86,6 +86,7 @@
         self.memory_handler.close()
         self.stream_handler.close()
         self.temp_handler.close()
+        self.devnull.close()
         super(PostArgParseSetupTest, self).tearDown()
 
     def test_common(self):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-0.28.0/certbot/tests/main_test.py 
new/certbot-0.29.1/certbot/tests/main_test.py
--- old/certbot-0.28.0/certbot/tests/main_test.py       2018-11-07 
22:14:56.000000000 +0100
+++ new/certbot-0.29.1/certbot/tests/main_test.py       2018-12-06 
00:47:58.000000000 +0100
@@ -520,6 +520,8 @@
                               '--work-dir', self.config.work_dir,
                               '--logs-dir', self.config.logs_dir, '--text']
 
+        self.mock_sleep = mock.patch('time.sleep').start()
+
     def tearDown(self):
         # Reset globals in cli
         reload_module(cli)
@@ -944,8 +946,8 @@
     @mock.patch('certbot.crypto_util.notAfter')
     @test_util.patch_get_utility()
     def test_certonly_new_request_success(self, mock_get_utility, 
mock_notAfter):
-        cert_path = '/etc/letsencrypt/live/foo.bar'
-        key_path = '/etc/letsencrypt/live/baz.qux'
+        cert_path = os.path.normpath(os.path.join(self.config.config_dir, 
'live/foo.bar'))
+        key_path = os.path.normpath(os.path.join(self.config.config_dir, 
'live/baz.qux'))
         date = '1970-01-01'
         mock_notAfter().date.return_value = date
 
@@ -975,7 +977,8 @@
                              reuse_key=False):
         # pylint: disable=too-many-locals,too-many-arguments,too-many-branches
         cert_path = test_util.vector_path('cert_512.pem')
-        chain_path = '/etc/letsencrypt/live/foo.bar/fullchain.pem'
+        chain_path = os.path.normpath(os.path.join(self.config.config_dir,
+                                                   
'live/foo.bar/fullchain.pem'))
         mock_lineage = mock.MagicMock(cert=cert_path, fullchain=chain_path,
                                       cert_path=cert_path, 
fullchain_path=chain_path)
         mock_lineage.should_autorenew.return_value = due_for_renewal
@@ -1092,6 +1095,26 @@
         args = ["renew", "--reuse-key"]
         self._test_renewal_common(True, [], args=args, should_renew=True, 
reuse_key=True)
 
+    @mock.patch('sys.stdin')
+    def test_noninteractive_renewal_delay(self, stdin):
+        stdin.isatty.return_value = False
+        test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf')
+        args = ["renew", "--dry-run", "-tvv"]
+        self._test_renewal_common(True, [], args=args, should_renew=True)
+        self.assertEqual(self.mock_sleep.call_count, 1)
+        # in main.py:
+        #     sleep_time = random.randint(1, 60*8)
+        sleep_call_arg = self.mock_sleep.call_args[0][0]
+        self.assertTrue(1 <= sleep_call_arg <= 60*8)
+
+    @mock.patch('sys.stdin')
+    def test_interactive_no_renewal_delay(self, stdin):
+        stdin.isatty.return_value = True
+        test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf')
+        args = ["renew", "--dry-run", "-tvv"]
+        self._test_renewal_common(True, [], args=args, should_renew=True)
+        self.assertEqual(self.mock_sleep.call_count, 0)
+
     @mock.patch('certbot.renewal.should_renew')
     def test_renew_skips_recent_certs(self, should_renew):
         should_renew.return_value = False
@@ -1652,7 +1675,7 @@
         mock_lineage.return_value = 
mock.MagicMock(chain_path="/tmp/nonexistent")
         self._call(['enhance', '--auto-hsts'])
         self.assertTrue(self.mockinstaller.enable_autohsts.called)
-        self.assertEquals(self.mockinstaller.enable_autohsts.call_args[0][1],
+        self.assertEqual(self.mockinstaller.enable_autohsts.call_args[0][1],
                           ["example.com", "another.tld"])
 
     @mock.patch('certbot.cert_manager.lineage_for_certname')
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-0.28.0/certbot/tests/ocsp_test.py 
new/certbot-0.29.1/certbot/tests/ocsp_test.py
--- old/certbot-0.28.0/certbot/tests/ocsp_test.py       2018-11-07 
22:14:56.000000000 +0100
+++ new/certbot-0.29.1/certbot/tests/ocsp_test.py       2018-12-06 
00:47:58.000000000 +0100
@@ -96,15 +96,15 @@
         self.assertEqual(ocsp._translate_ocsp_query(*openssl_happy), False)
         self.assertEqual(ocsp._translate_ocsp_query(*openssl_confused), False)
         self.assertEqual(mock_log.debug.call_count, 1)
-        self.assertEqual(mock_log.warn.call_count, 0)
+        self.assertEqual(mock_log.warning.call_count, 0)
         mock_log.debug.call_count = 0
         self.assertEqual(ocsp._translate_ocsp_query(*openssl_unknown), False)
         self.assertEqual(mock_log.debug.call_count, 1)
-        self.assertEqual(mock_log.warn.call_count, 0)
+        self.assertEqual(mock_log.warning.call_count, 0)
         self.assertEqual(ocsp._translate_ocsp_query(*openssl_expired_ocsp), 
False)
         self.assertEqual(mock_log.debug.call_count, 2)
         self.assertEqual(ocsp._translate_ocsp_query(*openssl_broken), False)
-        self.assertEqual(mock_log.warn.call_count, 1)
+        self.assertEqual(mock_log.warning.call_count, 1)
         mock_log.info.call_count = 0
         self.assertEqual(ocsp._translate_ocsp_query(*openssl_revoked), True)
         self.assertEqual(mock_log.info.call_count, 0)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-0.28.0/certbot/tests/renewupdater_test.py 
new/certbot-0.29.1/certbot/tests/renewupdater_test.py
--- old/certbot-0.28.0/certbot/tests/renewupdater_test.py       2018-11-07 
22:14:56.000000000 +0100
+++ new/certbot-0.29.1/certbot/tests/renewupdater_test.py       2018-12-06 
00:47:58.000000000 +0100
@@ -53,7 +53,7 @@
         self.config.dry_run = True
         updater.run_generic_updaters(self.config, None, None)
         self.assertTrue(mock_log.called)
-        self.assertEquals(mock_log.call_args[0][0],
+        self.assertEqual(mock_log.call_args[0][0],
                           "Skipping updaters in dry-run mode.")
 
     @mock.patch("certbot.updater.logger.debug")
@@ -61,7 +61,7 @@
         self.config.dry_run = True
         updater.run_renewal_deployer(self.config, None, None)
         self.assertTrue(mock_log.called)
-        self.assertEquals(mock_log.call_args[0][0],
+        self.assertEqual(mock_log.call_args[0][0],
                           "Skipping renewal deployer in dry-run mode.")
 
     @mock.patch('certbot.plugins.selection.get_unprepared_installer')
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-0.28.0/certbot/tests/storage_test.py 
new/certbot-0.29.1/certbot/tests/storage_test.py
--- old/certbot-0.28.0/certbot/tests/storage_test.py    2018-11-07 
22:14:56.000000000 +0100
+++ new/certbot-0.29.1/certbot/tests/storage_test.py    2018-12-06 
00:47:58.000000000 +0100
@@ -13,6 +13,7 @@
 
 import certbot
 from certbot import cli
+from certbot import compat
 from certbot import errors
 from certbot.storage import ALL_FOUR
 
@@ -73,9 +74,8 @@
         # We also create a file that isn't a renewal config in the same
         # location to test that logic that reads in all-and-only renewal
         # configs will ignore it and NOT attempt to parse it.
-        junk = open(os.path.join(self.config.config_dir, "renewal", 
"IGNORE.THIS"), "w")
-        junk.write("This file should be ignored!")
-        junk.close()
+        with open(os.path.join(self.config.config_dir, "renewal", 
"IGNORE.THIS"), "w") as junk:
+            junk.write("This file should be ignored!")
 
         self.defaults = configobj.ConfigObj()
 
@@ -92,6 +92,8 @@
                    link)
         with open(link, "wb") as f:
             f.write(kind.encode('ascii') if value is None else value)
+        if kind == "privkey":
+            os.chmod(link, 0o600)
 
     def _write_out_ex_kinds(self):
         for kind in ALL_FOUR:
@@ -264,12 +266,12 @@
         mock_has_pending.return_value = False
         self.assertEqual(self.test_rc.ensure_deployed(), True)
         self.assertEqual(mock_update.call_count, 0)
-        self.assertEqual(mock_logger.warn.call_count, 0)
+        self.assertEqual(mock_logger.warning.call_count, 0)
 
         mock_has_pending.return_value = True
         self.assertEqual(self.test_rc.ensure_deployed(), False)
         self.assertEqual(mock_update.call_count, 1)
-        self.assertEqual(mock_logger.warn.call_count, 1)
+        self.assertEqual(mock_logger.warning.call_count, 1)
 
 
     def test_update_link_to(self):
@@ -544,6 +546,47 @@
         self.assertFalse(os.path.islink(self.test_rc.version("privkey", 10)))
         self.assertFalse(os.path.exists(temp_config_file))
 
+    @test_util.broken_on_windows
+    @mock.patch("certbot.storage.relevant_values")
+    def test_save_successor_maintains_group_mode(self, mock_rv):
+        # Mock relevant_values() to claim that all values are relevant here
+        # (to avoid instantiating parser)
+        mock_rv.side_effect = lambda x: x
+        for kind in ALL_FOUR:
+            self._write_out_kind(kind, 1)
+        self.test_rc.update_all_links_to(1)
+        self.assertTrue(compat.compare_file_modes(
+            os.stat(self.test_rc.version("privkey", 1)).st_mode, 0o600))
+        os.chmod(self.test_rc.version("privkey", 1), 0o444)
+        # If no new key, permissions should be the same (we didn't write any 
keys)
+        self.test_rc.save_successor(1, b"newcert", None, b"new chain", 
self.config)
+        self.assertTrue(compat.compare_file_modes(
+            os.stat(self.test_rc.version("privkey", 2)).st_mode, 0o444))
+        # If new key, permissions should be kept as 644
+        self.test_rc.save_successor(2, b"newcert", b"new_privkey", b"new 
chain", self.config)
+        self.assertTrue(compat.compare_file_modes(
+            os.stat(self.test_rc.version("privkey", 3)).st_mode, 0o644))
+        # If permissions reverted, next renewal will also revert permissions 
of new key
+        os.chmod(self.test_rc.version("privkey", 3), 0o400)
+        self.test_rc.save_successor(3, b"newcert", b"new_privkey", b"new 
chain", self.config)
+        self.assertTrue(compat.compare_file_modes(
+            os.stat(self.test_rc.version("privkey", 4)).st_mode, 0o600))
+
+    @test_util.broken_on_windows
+    @mock.patch("certbot.storage.relevant_values")
+    @mock.patch("certbot.storage.os.chown")
+    def test_save_successor_maintains_gid(self, mock_chown, mock_rv):
+        # Mock relevant_values() to claim that all values are relevant here
+        # (to avoid instantiating parser)
+        mock_rv.side_effect = lambda x: x
+        for kind in ALL_FOUR:
+            self._write_out_kind(kind, 1)
+        self.test_rc.update_all_links_to(1)
+        self.test_rc.save_successor(1, b"newcert", None, b"new chain", 
self.config)
+        self.assertFalse(mock_chown.called)
+        self.test_rc.save_successor(2, b"newcert", b"new_privkey", b"new 
chain", self.config)
+        self.assertTrue(mock_chown.called)
+
     def _test_relevant_values_common(self, values):
         defaults = dict((option, cli.flag_default(option))
                         for option in ("authenticator", "installer",
@@ -630,6 +673,7 @@
             self.config.live_dir, "README")))
         self.assertTrue(os.path.exists(os.path.join(
             self.config.live_dir, "the-lineage.com", "README")))
+        
self.assertTrue(compat.compare_file_modes(os.stat(result.key_path).st_mode, 
0o600))
         with open(result.fullchain, "rb") as f:
             self.assertEqual(f.read(), b"cert" + b"chain")
         # Let's do it again and make sure it makes a different lineage
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-0.28.0/certbot/tests/util.py 
new/certbot-0.29.1/certbot/tests/util.py
--- old/certbot-0.28.0/certbot/tests/util.py    2018-11-07 22:14:56.000000000 
+0100
+++ new/certbot-0.29.1/certbot/tests/util.py    2018-12-06 00:47:58.000000000 
+0100
@@ -328,15 +328,16 @@
 
     def tearDown(self):
         """Execute after test"""
-        # Then we have various files which are not correctly closed at the 
time of tearDown.
-        # On Windows, it is visible for the same reasons as above.
+        # On Windows we have various files which are not correctly closed at 
the time of tearDown.
         # For know, we log them until a proper file close handling is written.
+        # Useful for development only, so no warning when we are on a CI 
process.
         def onerror_handler(_, path, excinfo):
             """On error handler"""
-            message = ('Following error occurred when deleting the tempdir {0}'
-                       ' for path {1} during tearDown process: {2}'
-                       .format(self.tempdir, path, str(excinfo)))
-            warnings.warn(message)
+            if not os.environ.get('APPVEYOR'): # pragma: no cover
+                message = ('Following error occurred when deleting the tempdir 
{0}'
+                           ' for path {1} during tearDown process: {2}'
+                           .format(self.tempdir, path, str(excinfo)))
+                warnings.warn(message)
         shutil.rmtree(self.tempdir, onerror=onerror_handler)
 
 class ConfigTestCase(TempDirTestCase):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-0.28.0/certbot/tests/util_test.py 
new/certbot-0.29.1/certbot/tests/util_test.py
--- old/certbot-0.28.0/certbot/tests/util_test.py       2018-11-07 
22:14:56.000000000 +0100
+++ new/certbot-0.29.1/certbot/tests/util_test.py       2018-12-06 
00:47:58.000000000 +0100
@@ -210,16 +210,21 @@
         fd, name = self._call()
         fd.write("bar")
         fd.close()
-        self.assertEqual(open(name).read(), "bar")
+        with open(name) as f:
+            self.assertEqual(f.read(), "bar")
 
     def test_right_mode(self):
-        self.assertTrue(compat.compare_file_modes(0o700, 
os.stat(self._call(0o700)[1]).st_mode))
-        self.assertTrue(compat.compare_file_modes(0o600, 
os.stat(self._call(0o600)[1]).st_mode))
+        fd1, name1 = self._call(0o700)
+        fd2, name2 = self._call(0o600)
+        self.assertTrue(compat.compare_file_modes(0o700, 
os.stat(name1).st_mode))
+        self.assertTrue(compat.compare_file_modes(0o600, 
os.stat(name2).st_mode))
+        fd1.close()
+        fd2.close()
 
     def test_default_exists(self):
-        name1 = self._call()[1]  # create 0000_foo.txt
-        name2 = self._call()[1]
-        name3 = self._call()[1]
+        fd1, name1 = self._call()  # create 0000_foo.txt
+        fd2, name2 = self._call()
+        fd3, name3 = self._call()
 
         self.assertNotEqual(name1, name2)
         self.assertNotEqual(name1, name3)
@@ -236,6 +241,10 @@
         basename3 = os.path.basename(name3)
         self.assertTrue(basename3.endswith("foo.txt"))
 
+        fd1.close()
+        fd2.close()
+        fd3.close()
+
 
 try:
     file_type = file
@@ -255,13 +264,18 @@
         f, path = self._call("wow")
         self.assertTrue(isinstance(f, file_type))
         self.assertEqual(os.path.join(self.tempdir, "wow.conf"), path)
+        f.close()
 
     def test_multiple(self):
+        items = []
         for _ in six.moves.range(10):
-            f, name = self._call("wow")
+            items.append(self._call("wow"))
+        f, name = items[-1]
         self.assertTrue(isinstance(f, file_type))
         self.assertTrue(isinstance(name, six.string_types))
         self.assertTrue("wow-0009.conf" in name)
+        for f, _ in items:
+            f.close()
 
     @mock.patch("certbot.util.os.fdopen")
     def test_failure(self, mock_fdopen):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-0.28.0/certbot.egg-info/PKG-INFO 
new/certbot-0.29.1/certbot.egg-info/PKG-INFO
--- old/certbot-0.28.0/certbot.egg-info/PKG-INFO        2018-11-07 
22:14:58.000000000 +0100
+++ new/certbot-0.29.1/certbot.egg-info/PKG-INFO        2018-12-06 
00:47:59.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: certbot
-Version: 0.28.0
+Version: 0.29.1
 Summary: ACME client
 Home-page: https://github.com/letsencrypt/letsencrypt
 Author: Certbot Project
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-0.28.0/certbot.egg-info/requires.txt 
new/certbot-0.29.1/certbot.egg-info/requires.txt
--- old/certbot-0.28.0/certbot.egg-info/requires.txt    2018-11-07 
22:14:58.000000000 +0100
+++ new/certbot-0.29.1/certbot.egg-info/requires.txt    2018-12-06 
00:47:59.000000000 +0100
@@ -1,4 +1,4 @@
-acme>=0.26.0
+acme>=0.29.0
 ConfigArgParse>=0.9.3
 configobj
 cryptography>=1.2
@@ -29,5 +29,5 @@
 
 [docs]
 repoze.sphinx.autointerface
-Sphinx>=1.6
+Sphinx>=1.2
 sphinx_rtd_theme
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-0.28.0/docs/cli-help.txt 
new/certbot-0.29.1/docs/cli-help.txt
--- old/certbot-0.28.0/docs/cli-help.txt        2018-11-07 22:14:56.000000000 
+0100
+++ new/certbot-0.29.1/docs/cli-help.txt        2018-12-06 00:47:58.000000000 
+0100
@@ -24,7 +24,7 @@
 
 manage certificates:
     certificates    Display information about certificates you have from 
Certbot
-    revoke          Revoke a certificate (supply --cert-path)
+    revoke          Revoke a certificate (supply --cert-path or --cert-name)
     delete          Delete a certificate
 
 manage your account with Let's Encrypt:
@@ -67,6 +67,10 @@
                         with the same name. In the case of a name collision it
                         will append a number like 0001 to the file path name.
                         (default: Ask)
+  --eab-kid EAB_KID     Key Identifier for External Account Binding (default:
+                        None)
+  --eab-hmac-key EAB_HMAC_KEY
+                        HMAC key for External Account Binding (default: None)
   --cert-name CERTNAME  Certificate name to apply. This name is used by
                         Certbot for housekeeping and in file paths; it doesn't
                         affect the content of the certificate itself. To see
@@ -108,7 +112,7 @@
                         case, and to know when to deprecate support for past
                         Python versions and flags. If you wish to hide this
                         information from the Let's Encrypt server, set this to
-                        "". (default: CertbotACMEClient/0.27.1
+                        "". (default: CertbotACMEClient/0.29.0
                         (certbot(-auto); OS_NAME OS_VERSION) Authenticator/XXX
                         Installer/YYY (SUBCOMMAND; flags: FLAGS)
                         Py/major.minor.patchlevel). The flags encoded in the
@@ -248,8 +252,8 @@
                         None)
   --config-dir CONFIG_DIR
                         Configuration directory. (default: /etc/letsencrypt)
-  --work-dir WORK_DIR   Working directory. (default: /var/lib/letsencrypt)
-  --logs-dir LOGS_DIR   Logs directory. (default: /var/log/letsencrypt)
+  --work-dir WORK_DIR   Working directory. (default: /var/letsencrypt/lib)
+  --logs-dir LOGS_DIR   Logs directory. (default: /var/letsencrypt/log)
   --server SERVER       ACME Directory Resource URI. (default:
                         https://acme-v02.api.letsencrypt.org/directory)
 
@@ -261,7 +265,8 @@
   delete                Clean up all files related to a certificate
   renew                 Renew all certificates (or one specified with --cert-
                         name)
-  revoke                Revoke a certificate specified with --cert-path
+  revoke                Revoke a certificate specified with --cert-path or
+                        --cert-name
   update_symlinks       Recreate symlinks in your /etc/letsencrypt/live/
                         directory
 
@@ -472,13 +477,12 @@
                         using Sakura Cloud for DNS). (default: False)
 
 apache:
-  Apache Web Server plugin - Beta
+  Apache Web Server plugin
 
   --apache-enmod APACHE_ENMOD
-                        Path to the Apache 'a2enmod' binary (default: a2enmod)
+                        Path to the Apache 'a2enmod' binary (default: None)
   --apache-dismod APACHE_DISMOD
-                        Path to the Apache 'a2dismod' binary (default:
-                        a2dismod)
+                        Path to the Apache 'a2dismod' binary (default: None)
   --apache-le-vhost-ext APACHE_LE_VHOST_EXT
                         SSL vhost configuration extension (default: -le-
                         ssl.conf)
@@ -492,25 +496,16 @@
                         /var/log/apache2)
   --apache-challenge-location APACHE_CHALLENGE_LOCATION
                         Directory path for challenge configuration (default:
-                        /etc/apache2)
+                        /etc/apache2/other)
   --apache-handle-modules APACHE_HANDLE_MODULES
                         Let installer handle enabling required modules for you
-                        (Only Ubuntu/Debian currently) (default: True)
+                        (Only Ubuntu/Debian currently) (default: False)
   --apache-handle-sites APACHE_HANDLE_SITES
                         Let installer handle enabling sites for you (Only
-                        Ubuntu/Debian currently) (default: True)
+                        Ubuntu/Debian currently) (default: False)
   --apache-ctl APACHE_CTL
                         Full path to Apache control script (default:
-                        apache2ctl)
-
-certbot-route53:auth:
-  Obtain certificates using a DNS TXT record (if you are using AWS Route53
-  for DNS).
-
-  --certbot-route53:auth-propagation-seconds 
CERTBOT_ROUTE53:AUTH_PROPAGATION_SECONDS
-                        The number of seconds to wait for DNS to propagate
-                        before asking the ACME server to verify the DNS
-                        record. (default: 10)
+                        apachectl)
 
 dns-cloudflare:
   Obtain certificates using a DNS TXT record (if you are using Cloudflare
@@ -602,7 +597,7 @@
   --dns-linode-propagation-seconds DNS_LINODE_PROPAGATION_SECONDS
                         The number of seconds to wait for DNS to propagate
                         before asking the ACME server to verify the DNS
-                        record. (default: 960)
+                        record. (default: 1200)
   --dns-linode-credentials DNS_LINODE_CREDENTIALS
                         Linode credentials INI file. (default: None)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-0.28.0/docs/conf.py 
new/certbot-0.29.1/docs/conf.py
--- old/certbot-0.28.0/docs/conf.py     2018-11-07 22:14:56.000000000 +0100
+++ new/certbot-0.29.1/docs/conf.py     2018-12-06 00:47:58.000000000 +0100
@@ -17,6 +17,8 @@
 import re
 import sys
 
+import sphinx
+
 
 here = os.path.abspath(os.path.dirname(__file__))
 
@@ -33,14 +35,13 @@
 # -- General configuration ------------------------------------------------
 
 # If your documentation needs a minimal Sphinx version, state it here.
-needs_sphinx = '1.0'
+needs_sphinx = '1.2'
 
 # Add any Sphinx extension module names here, as strings. They can be
 # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
 # ones.
 extensions = [
     'sphinx.ext.autodoc',
-    'sphinx.ext.imgconverter',
     'sphinx.ext.intersphinx',
     'sphinx.ext.todo',
     'sphinx.ext.coverage',
@@ -48,6 +49,9 @@
     'repoze.sphinx.autointerface',
 ]
 
+if sphinx.version_info >= (1, 6):
+    extensions.append('sphinx.ext.imgconverter')
+
 autodoc_member_order = 'bysource'
 autodoc_default_flags = ['show-inheritance', 'private-members']
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-0.28.0/docs/contributing.rst 
new/certbot-0.29.1/docs/contributing.rst
--- old/certbot-0.28.0/docs/contributing.rst    2018-11-07 22:14:56.000000000 
+0100
+++ new/certbot-0.29.1/docs/contributing.rst    2018-12-06 00:47:58.000000000 
+0100
@@ -38,13 +38,13 @@
 
    cd certbot
    ./certbot-auto --debug --os-packages-only
-   tools/venv.sh
+   python tools/venv.py
 
-If you have Python3 available and want to use it, run the ``venv3.sh`` script.
+If you have Python3 available and want to use it, run the ``venv3.py`` script.
 
 .. code-block:: shell
 
-   tools/venv3.sh
+   python tools/venv3.py
 
 .. note:: You may need to repeat this when
   Certbot's dependencies change or when a new plugin is introduced.
@@ -353,13 +353,16 @@
 
 1. Write your code!
 2. Make sure your environment is set up properly and that you're in your
-   virtualenv. You can do this by running ``./tools/venv.sh``.
+   virtualenv. You can do this by running ``pip tools/venv.py``.
    (this is a **very important** step)
 3. Run ``tox -e lint`` to check for pylint errors. Fix any errors.
 4. Run ``tox --skip-missing-interpreters`` to run the entire test suite
    including coverage. The ``--skip-missing-interpreters`` argument ignores
    missing versions of Python needed for running the tests. Fix any errors.
-5. Submit the PR.
+5. Submit the PR. Once your PR is open, please do not force push to the branch
+   containing your pull request to squash or amend commits. We use `squash
+   merges <https://github.com/blog/2141-squash-your-commits>`_ on PRs and
+   rewriting commits makes changes harder to track between reviews.
 6. Did your tests pass on Travis? If they didn't, fix any errors.
 
 Asking for help
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-0.28.0/docs/using.rst 
new/certbot-0.29.1/docs/using.rst
--- old/certbot-0.28.0/docs/using.rst   2018-11-07 22:14:56.000000000 +0100
+++ new/certbot-0.29.1/docs/using.rst   2018-12-06 00:47:58.000000000 +0100
@@ -696,7 +696,9 @@
 ==========================
 
 All generated keys and issued certificates can be found in
-``/etc/letsencrypt/live/$domain``. Rather than copying, please point
+``/etc/letsencrypt/live/$domain``. In the case of creating a SAN certificate 
+with multiple alternative names, ``$domain`` is the first domain passed in 
+via -d parameter. Rather than copying, please point
 your (web) server configuration directly to those files (or create
 symlinks). During the renewal_, ``/etc/letsencrypt/live`` is updated
 with the latest necessary files.
@@ -715,6 +717,10 @@
      put it into a safe, however - your server still needs to access
      this file in order for SSL/TLS to work.
 
+  .. note:: As of Certbot version 0.29.0, private keys for new certificate
+     default to ``0600``. Any changes to the group mode or group owner (gid)
+     of this file will be preserved on renewals.
+
   This is what Apache needs for `SSLCertificateKeyFile
   <https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslcertificatekeyfile>`_,
   and Nginx for `ssl_certificate_key
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot-0.28.0/setup.py new/certbot-0.29.1/setup.py
--- old/certbot-0.28.0/setup.py 2018-11-07 22:14:57.000000000 +0100
+++ new/certbot-0.29.1/setup.py 2018-12-06 00:47:59.000000000 +0100
@@ -31,7 +31,7 @@
 # specified here to avoid masking the more specific request requirements in
 # acme. See https://github.com/pypa/pip/issues/988 for more info.
 install_requires = [
-    'acme>=0.26.0',
+    'acme>=0.29.0',
     # We technically need ConfigArgParse 0.10.0 for Python 2.6 support, but
     # saying so here causes a runtime error against our temporary fork of 0.9.3
     # in which we added 2.6 support (see #2243), so we relax the requirement.
@@ -68,9 +68,10 @@
 ]
 
 docs_extras = [
+    # If you have Sphinx<1.5.1, you need docutils<0.13.1
+    # https://github.com/sphinx-doc/sphinx/issues/3212
     'repoze.sphinx.autointerface',
-    # sphinx.ext.imgconverter
-    'Sphinx >=1.6',
+    'Sphinx>=1.2', # Annotation support
     'sphinx_rtd_theme',
 ]
 


Reply via email to