Browse Source

Remove various bits of compatibility code for Python <3.6 (#9879)

I went through and removed a bunch of cruft that was lying around for compatibility with old Python versions. This PR also will now prevent Synapse from starting unless you're running Python 3.6+.
tags/v1.33.0rc1
Andrew Morgan 3 years ago
committed by GitHub
parent
commit
fe604a022a
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 29 additions and 98 deletions
  1. +1
    -0
      changelog.d/9879.misc
  2. +0
    -1
      mypy.ini
  3. +2
    -2
      synapse/__init__.py
  4. +2
    -7
      synapse/python_dependencies.py
  5. +2
    -1
      synapse/rest/admin/users.py
  6. +1
    -9
      synapse/rest/consent/consent_resource.py
  7. +1
    -1
      synapse/rest/media/v1/filepath.py
  8. +0
    -44
      synapse/secrets.py
  9. +0
    -5
      synapse/server.py
  10. +1
    -1
      synapse/storage/_base.py
  11. +6
    -9
      synapse/storage/database.py
  12. +1
    -1
      synapse/util/caches/response_cache.py
  13. +6
    -9
      tests/rest/admin/test_user.py
  14. +2
    -1
      tests/storage/test__base.py
  15. +1
    -1
      tests/unittest.py
  16. +3
    -6
      tox.ini

+ 1
- 0
changelog.d/9879.misc View File

@@ -0,0 +1 @@
Remove backwards-compatibility code for Python versions < 3.6.

+ 0
- 1
mypy.ini View File

@@ -41,7 +41,6 @@ files =
synapse/push, synapse/push,
synapse/replication, synapse/replication,
synapse/rest, synapse/rest,
synapse/secrets.py,
synapse/server.py, synapse/server.py,
synapse/server_notices, synapse/server_notices,
synapse/spam_checker_api, synapse/spam_checker_api,


+ 2
- 2
synapse/__init__.py View File

@@ -21,8 +21,8 @@ import os
import sys import sys


# Check that we're not running on an unsupported Python version. # Check that we're not running on an unsupported Python version.
if sys.version_info < (3, 5):
print("Synapse requires Python 3.5 or above.")
if sys.version_info < (3, 6):
print("Synapse requires Python 3.6 or above.")
sys.exit(1) sys.exit(1)


# Twisted and canonicaljson will fail to import when this file is executed to # Twisted and canonicaljson will fail to import when this file is executed to


+ 2
- 7
synapse/python_dependencies.py View File

@@ -85,7 +85,7 @@ REQUIREMENTS = [
"typing-extensions>=3.7.4", "typing-extensions>=3.7.4",
# We enforce that we have a `cryptography` version that bundles an `openssl` # We enforce that we have a `cryptography` version that bundles an `openssl`
# with the latest security patches. # with the latest security patches.
"cryptography>=3.4.7;python_version>='3.6'",
"cryptography>=3.4.7",
] ]


CONDITIONAL_REQUIREMENTS = { CONDITIONAL_REQUIREMENTS = {
@@ -100,14 +100,9 @@ CONDITIONAL_REQUIREMENTS = {
# that use the protocol, such as Let's Encrypt. # that use the protocol, such as Let's Encrypt.
"acme": [ "acme": [
"txacme>=0.9.2", "txacme>=0.9.2",
# txacme depends on eliot. Eliot 1.8.0 is incompatible with
# python 3.5.2, as per https://github.com/itamarst/eliot/issues/418
"eliot<1.8.0;python_version<'3.5.3'",
], ],
"saml2": [ "saml2": [
# pysaml2 6.4.0 is incompatible with Python 3.5 (see https://github.com/IdentityPython/pysaml2/issues/749)
"pysaml2>=4.5.0,<6.4.0;python_version<'3.6'",
"pysaml2>=4.5.0;python_version>='3.6'",
"pysaml2>=4.5.0",
], ],
"oidc": ["authlib>=0.14.0"], "oidc": ["authlib>=0.14.0"],
# systemd-python is necessary for logging to the systemd journal via # systemd-python is necessary for logging to the systemd journal via


+ 2
- 1
synapse/rest/admin/users.py View File

@@ -14,6 +14,7 @@
import hashlib import hashlib
import hmac import hmac
import logging import logging
import secrets
from http import HTTPStatus from http import HTTPStatus
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple from typing import TYPE_CHECKING, Dict, List, Optional, Tuple


@@ -375,7 +376,7 @@ class UserRegisterServlet(RestServlet):
""" """
self._clear_old_nonces() self._clear_old_nonces()


nonce = self.hs.get_secrets().token_hex(64)
nonce = secrets.token_hex(64)
self.nonces[nonce] = int(self.reactor.seconds()) self.nonces[nonce] = int(self.reactor.seconds())
return 200, {"nonce": nonce} return 200, {"nonce": nonce}




+ 1
- 9
synapse/rest/consent/consent_resource.py View File

@@ -32,14 +32,6 @@ TEMPLATE_LANGUAGE = "en"


logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)


# use hmac.compare_digest if we have it (python 2.7.7), else just use equality
if hasattr(hmac, "compare_digest"):
compare_digest = hmac.compare_digest
else:

def compare_digest(a, b):
return a == b



class ConsentResource(DirectServeHtmlResource): class ConsentResource(DirectServeHtmlResource):
"""A twisted Resource to display a privacy policy and gather consent to it """A twisted Resource to display a privacy policy and gather consent to it
@@ -209,5 +201,5 @@ class ConsentResource(DirectServeHtmlResource):
.encode("ascii") .encode("ascii")
) )


if not compare_digest(want_mac, userhmac):
if not hmac.compare_digest(want_mac, userhmac):
raise SynapseError(HTTPStatus.FORBIDDEN, "HMAC incorrect") raise SynapseError(HTTPStatus.FORBIDDEN, "HMAC incorrect")

+ 1
- 1
synapse/rest/media/v1/filepath.py View File

@@ -21,7 +21,7 @@ from typing import Callable, List
NEW_FORMAT_ID_RE = re.compile(r"^\d\d\d\d-\d\d-\d\d") NEW_FORMAT_ID_RE = re.compile(r"^\d\d\d\d-\d\d-\d\d")




def _wrap_in_base_path(func: "Callable[..., str]") -> "Callable[..., str]":
def _wrap_in_base_path(func: Callable[..., str]) -> Callable[..., str]:
"""Takes a function that returns a relative path and turns it into an """Takes a function that returns a relative path and turns it into an
absolute path based on the location of the primary media store absolute path based on the location of the primary media store
""" """


+ 0
- 44
synapse/secrets.py View File

@@ -1,44 +0,0 @@
# Copyright 2018 New Vector Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Injectable secrets module for Synapse.

See https://docs.python.org/3/library/secrets.html#module-secrets for the API
used in Python 3.6, and the API emulated in Python 2.7.
"""
import sys

# secrets is available since python 3.6
if sys.version_info[0:2] >= (3, 6):
import secrets

class Secrets:
def token_bytes(self, nbytes: int = 32) -> bytes:
return secrets.token_bytes(nbytes)

def token_hex(self, nbytes: int = 32) -> str:
return secrets.token_hex(nbytes)


else:
import binascii
import os

class Secrets:
def token_bytes(self, nbytes: int = 32) -> bytes:
return os.urandom(nbytes)

def token_hex(self, nbytes: int = 32) -> str:
return binascii.hexlify(self.token_bytes(nbytes)).decode("ascii")

+ 0
- 5
synapse/server.py View File

@@ -126,7 +126,6 @@ from synapse.rest.media.v1.media_repository import (
MediaRepository, MediaRepository,
MediaRepositoryResource, MediaRepositoryResource,
) )
from synapse.secrets import Secrets
from synapse.server_notices.server_notices_manager import ServerNoticesManager from synapse.server_notices.server_notices_manager import ServerNoticesManager
from synapse.server_notices.server_notices_sender import ServerNoticesSender from synapse.server_notices.server_notices_sender import ServerNoticesSender
from synapse.server_notices.worker_server_notices_sender import ( from synapse.server_notices.worker_server_notices_sender import (
@@ -641,10 +640,6 @@ class HomeServer(metaclass=abc.ABCMeta):
def get_groups_attestation_renewer(self) -> GroupAttestionRenewer: def get_groups_attestation_renewer(self) -> GroupAttestionRenewer:
return GroupAttestionRenewer(self) return GroupAttestionRenewer(self)


@cache_in_self
def get_secrets(self) -> Secrets:
return Secrets()

@cache_in_self @cache_in_self
def get_stats_handler(self) -> StatsHandler: def get_stats_handler(self) -> StatsHandler:
return StatsHandler(self) return StatsHandler(self)


+ 1
- 1
synapse/storage/_base.py View File

@@ -114,7 +114,7 @@ def db_to_json(db_content: Union[memoryview, bytes, bytearray, str]) -> Any:
db_content = db_content.tobytes() db_content = db_content.tobytes()


# Decode it to a Unicode string before feeding it to the JSON decoder, since # Decode it to a Unicode string before feeding it to the JSON decoder, since
# Python 3.5 does not support deserializing bytes.
# it only supports handling strings
if isinstance(db_content, (bytes, bytearray)): if isinstance(db_content, (bytes, bytearray)):
db_content = db_content.decode("utf8") db_content = db_content.decode("utf8")




+ 6
- 9
synapse/storage/database.py View File

@@ -171,10 +171,7 @@ class LoggingDatabaseConnection:




# The type of entry which goes on our after_callbacks and exception_callbacks lists. # The type of entry which goes on our after_callbacks and exception_callbacks lists.
#
# Python 3.5.2 doesn't support Callable with an ellipsis, so we wrap it in quotes so
# that mypy sees the type but the runtime python doesn't.
_CallbackListEntry = Tuple["Callable[..., None]", Iterable[Any], Dict[str, Any]]
_CallbackListEntry = Tuple[Callable[..., None], Iterable[Any], Dict[str, Any]]




R = TypeVar("R") R = TypeVar("R")
@@ -221,7 +218,7 @@ class LoggingTransaction:
self.after_callbacks = after_callbacks self.after_callbacks = after_callbacks
self.exception_callbacks = exception_callbacks self.exception_callbacks = exception_callbacks


def call_after(self, callback: "Callable[..., None]", *args: Any, **kwargs: Any):
def call_after(self, callback: Callable[..., None], *args: Any, **kwargs: Any):
"""Call the given callback on the main twisted thread after the """Call the given callback on the main twisted thread after the
transaction has finished. Used to invalidate the caches on the transaction has finished. Used to invalidate the caches on the
correct thread. correct thread.
@@ -233,7 +230,7 @@ class LoggingTransaction:
self.after_callbacks.append((callback, args, kwargs)) self.after_callbacks.append((callback, args, kwargs))


def call_on_exception( def call_on_exception(
self, callback: "Callable[..., None]", *args: Any, **kwargs: Any
self, callback: Callable[..., None], *args: Any, **kwargs: Any
): ):
# if self.exception_callbacks is None, that means that whatever constructed the # if self.exception_callbacks is None, that means that whatever constructed the
# LoggingTransaction isn't expecting there to be any callbacks; assert that # LoggingTransaction isn't expecting there to be any callbacks; assert that
@@ -485,7 +482,7 @@ class DatabasePool:
desc: str, desc: str,
after_callbacks: List[_CallbackListEntry], after_callbacks: List[_CallbackListEntry],
exception_callbacks: List[_CallbackListEntry], exception_callbacks: List[_CallbackListEntry],
func: "Callable[..., R]",
func: Callable[..., R],
*args: Any, *args: Any,
**kwargs: Any, **kwargs: Any,
) -> R: ) -> R:
@@ -618,7 +615,7 @@ class DatabasePool:
async def runInteraction( async def runInteraction(
self, self,
desc: str, desc: str,
func: "Callable[..., R]",
func: Callable[..., R],
*args: Any, *args: Any,
db_autocommit: bool = False, db_autocommit: bool = False,
**kwargs: Any, **kwargs: Any,
@@ -678,7 +675,7 @@ class DatabasePool:


async def runWithConnection( async def runWithConnection(
self, self,
func: "Callable[..., R]",
func: Callable[..., R],
*args: Any, *args: Any,
db_autocommit: bool = False, db_autocommit: bool = False,
**kwargs: Any, **kwargs: Any,


+ 1
- 1
synapse/util/caches/response_cache.py View File

@@ -110,7 +110,7 @@ class ResponseCache(Generic[T]):
return result.observe() return result.observe()


def wrap( def wrap(
self, key: T, callback: "Callable[..., Any]", *args: Any, **kwargs: Any
self, key: T, callback: Callable[..., Any], *args: Any, **kwargs: Any
) -> defer.Deferred: ) -> defer.Deferred:
"""Wrap together a *get* and *set* call, taking care of logcontexts """Wrap together a *get* and *set* call, taking care of logcontexts




+ 6
- 9
tests/rest/admin/test_user.py View File

@@ -18,7 +18,7 @@ import json
import urllib.parse import urllib.parse
from binascii import unhexlify from binascii import unhexlify
from typing import List, Optional from typing import List, Optional
from unittest.mock import Mock
from unittest.mock import Mock, patch


import synapse.rest.admin import synapse.rest.admin
from synapse.api.constants import UserTypes from synapse.api.constants import UserTypes
@@ -54,8 +54,6 @@ class UserRegisterTestCase(unittest.HomeserverTestCase):
self.datastore = Mock(return_value=Mock()) self.datastore = Mock(return_value=Mock())
self.datastore.get_current_state_deltas = Mock(return_value=(0, [])) self.datastore.get_current_state_deltas = Mock(return_value=(0, []))


self.secrets = Mock()

self.hs = self.setup_test_homeserver() self.hs = self.setup_test_homeserver()


self.hs.config.registration_shared_secret = "shared" self.hs.config.registration_shared_secret = "shared"
@@ -84,14 +82,13 @@ class UserRegisterTestCase(unittest.HomeserverTestCase):
Calling GET on the endpoint will return a randomised nonce, using the Calling GET on the endpoint will return a randomised nonce, using the
homeserver's secrets provider. homeserver's secrets provider.
""" """
secrets = Mock()
secrets.token_hex = Mock(return_value="abcd")

self.hs.get_secrets = Mock(return_value=secrets)
with patch("secrets.token_hex") as token_hex:
# Patch secrets.token_hex for the duration of this context
token_hex.return_value = "abcd"


channel = self.make_request("GET", self.url)
channel = self.make_request("GET", self.url)


self.assertEqual(channel.json_body, {"nonce": "abcd"})
self.assertEqual(channel.json_body, {"nonce": "abcd"})


def test_expired_nonce(self): def test_expired_nonce(self):
""" """


+ 2
- 1
tests/storage/test__base.py View File

@@ -13,6 +13,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.


import secrets


from tests import unittest from tests import unittest


@@ -21,7 +22,7 @@ class UpsertManyTests(unittest.HomeserverTestCase):
def prepare(self, reactor, clock, hs): def prepare(self, reactor, clock, hs):
self.storage = hs.get_datastore() self.storage = hs.get_datastore()


self.table_name = "table_" + hs.get_secrets().token_hex(6)
self.table_name = "table_" + secrets.token_hex(6)
self.get_success( self.get_success(
self.storage.db_pool.runInteraction( self.storage.db_pool.runInteraction(
"create", "create",


+ 1
- 1
tests/unittest.py View File

@@ -18,6 +18,7 @@ import hashlib
import hmac import hmac
import inspect import inspect
import logging import logging
import secrets
import time import time
from typing import Callable, Dict, Iterable, Optional, Tuple, Type, TypeVar, Union from typing import Callable, Dict, Iterable, Optional, Tuple, Type, TypeVar, Union
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
@@ -626,7 +627,6 @@ class HomeserverTestCase(TestCase):
str: The new event's ID. str: The new event's ID.
""" """
event_creator = self.hs.get_event_creation_handler() event_creator = self.hs.get_event_creation_handler()
secrets = self.hs.get_secrets()
requester = create_requester(user) requester = create_requester(user)


event, context = self.get_success( event, context = self.get_success(


+ 3
- 6
tox.ini View File

@@ -21,13 +21,11 @@ deps =
# installed on that). # installed on that).
# #
# anyway, make sure that we have a recent enough setuptools. # anyway, make sure that we have a recent enough setuptools.
setuptools>=18.5 ; python_version >= '3.6'
setuptools>=18.5,<51.0.0 ; python_version < '3.6'
setuptools>=18.5


# we also need a semi-recent version of pip, because old ones fail to # we also need a semi-recent version of pip, because old ones fail to
# install the "enum34" dependency of cryptography. # install the "enum34" dependency of cryptography.
pip>=10 ; python_version >= '3.6'
pip>=10,<21.0 ; python_version < '3.6'
pip>=10


# directories/files we run the linters on. # directories/files we run the linters on.
# if you update this list, make sure to do the same in scripts-dev/lint.sh # if you update this list, make sure to do the same in scripts-dev/lint.sh
@@ -168,8 +166,7 @@ skip_install = true
usedevelop = false usedevelop = false
deps = deps =
coverage coverage
pip>=10 ; python_version >= '3.6'
pip>=10,<21.0 ; python_version < '3.6'
pip>=10
commands= commands=
coverage combine coverage combine
coverage report coverage report


Loading…
Cancel
Save