diff --git a/changelog.d/12127.misc b/changelog.d/12127.misc
new file mode 100644
index 0000000000..e42eca63a8
--- /dev/null
+++ b/changelog.d/12127.misc
@@ -0,0 +1 @@
+Update release script to insert the previous version when writing "No significant changes" line in the changelog.
diff --git a/changelog.d/12129.misc b/changelog.d/12129.misc
new file mode 100644
index 0000000000..ce4213650c
--- /dev/null
+++ b/changelog.d/12129.misc
@@ -0,0 +1 @@
+Inspect application dependencies using `importlib.metadata` or its backport.
\ No newline at end of file
diff --git a/changelog.d/12141.bugfix b/changelog.d/12141.bugfix
new file mode 100644
index 0000000000..03a2507e2c
--- /dev/null
+++ b/changelog.d/12141.bugfix
@@ -0,0 +1 @@
+Fix a bug introduced in Synapse 1.54.0rc1 preventing the new module callbacks introduced in this release from being registered by modules.
diff --git a/changelog.d/12166.misc b/changelog.d/12166.misc
new file mode 100644
index 0000000000..24b4a7c7de
--- /dev/null
+++ b/changelog.d/12166.misc
@@ -0,0 +1 @@
+Relax the version guard for "packaging" added in #12088.
diff --git a/scripts-dev/release.py b/scripts-dev/release.py
index 4e1f99fee4..046453e65f 100755
--- a/scripts-dev/release.py
+++ b/scripts-dev/release.py
@@ -17,6 +17,8 @@
"""An interactive script for doing a release. See `cli()` below.
"""
+import glob
+import os
import re
import subprocess
import sys
@@ -209,8 +211,8 @@ def prepare():
with open("synapse/__init__.py", "w") as f:
f.write(parsed_synapse_ast.dumps())
- # Generate changelogs
- run_until_successful("python3 -m towncrier", shell=True)
+ # Generate changelogs.
+ generate_and_write_changelog(current_version)
# Generate debian changelogs
if parsed_new_version.pre is not None:
@@ -523,5 +525,29 @@ def get_changes_for_version(wanted_version: version.Version) -> str:
return "\n".join(version_changelog)
+def generate_and_write_changelog(current_version: version.Version):
+ # We do this by getting a draft so that we can edit it before writing to the
+ # changelog.
+ result = run_until_successful(
+ "python3 -m towncrier --draft", shell=True, capture_output=True
+ )
+ new_changes = result.stdout.decode("utf-8")
+ new_changes = new_changes.replace(
+ "No significant changes.", f"No significant changes since {current_version}."
+ )
+
+ # Prepend changes to changelog
+ with open("CHANGES.md", "r+") as f:
+ existing_content = f.read()
+ f.seek(0, 0)
+ f.write(new_changes)
+ f.write("\n")
+ f.write(existing_content)
+
+ # Remove all the news fragments
+ for f in glob.iglob("changelog.d/*.*"):
+ os.remove(f)
+
+
if __name__ == "__main__":
cli()
diff --git a/synapse/events/third_party_rules.py b/synapse/events/third_party_rules.py
index dd3104faf3..ede72ee876 100644
--- a/synapse/events/third_party_rules.py
+++ b/synapse/events/third_party_rules.py
@@ -174,7 +174,9 @@ class ThirdPartyEventRules:
] = None,
on_new_event: Optional[ON_NEW_EVENT_CALLBACK] = None,
on_profile_update: Optional[ON_PROFILE_UPDATE_CALLBACK] = None,
- on_deactivation: Optional[ON_USER_DEACTIVATION_STATUS_CHANGED_CALLBACK] = None,
+ on_user_deactivation_status_changed: Optional[
+ ON_USER_DEACTIVATION_STATUS_CHANGED_CALLBACK
+ ] = None,
) -> None:
"""Register callbacks from modules for each hook."""
if check_event_allowed is not None:
@@ -199,8 +201,10 @@ class ThirdPartyEventRules:
if on_profile_update is not None:
self._on_profile_update_callbacks.append(on_profile_update)
- if on_deactivation is not None:
- self._on_user_deactivation_status_changed_callbacks.append(on_deactivation)
+ if on_user_deactivation_status_changed is not None:
+ self._on_user_deactivation_status_changed_callbacks.append(
+ on_user_deactivation_status_changed,
+ )
async def check_event_allowed(
self, event: EventBase, context: EventContext
diff --git a/synapse/module_api/__init__.py b/synapse/module_api/__init__.py
index 7e46931869..c42eeedd87 100644
--- a/synapse/module_api/__init__.py
+++ b/synapse/module_api/__init__.py
@@ -59,6 +59,8 @@ from synapse.events.third_party_rules import (
CHECK_VISIBILITY_CAN_BE_MODIFIED_CALLBACK,
ON_CREATE_ROOM_CALLBACK,
ON_NEW_EVENT_CALLBACK,
+ ON_PROFILE_UPDATE_CALLBACK,
+ ON_USER_DEACTIVATION_STATUS_CHANGED_CALLBACK,
)
from synapse.handlers.account_validity import (
IS_USER_EXPIRED_CALLBACK,
@@ -281,6 +283,10 @@ class ModuleApi:
CHECK_VISIBILITY_CAN_BE_MODIFIED_CALLBACK
] = None,
on_new_event: Optional[ON_NEW_EVENT_CALLBACK] = None,
+ on_profile_update: Optional[ON_PROFILE_UPDATE_CALLBACK] = None,
+ on_user_deactivation_status_changed: Optional[
+ ON_USER_DEACTIVATION_STATUS_CHANGED_CALLBACK
+ ] = None,
) -> None:
"""Registers callbacks for third party event rules capabilities.
@@ -292,6 +298,8 @@ class ModuleApi:
check_threepid_can_be_invited=check_threepid_can_be_invited,
check_visibility_can_be_modified=check_visibility_can_be_modified,
on_new_event=on_new_event,
+ on_profile_update=on_profile_update,
+ on_user_deactivation_status_changed=on_user_deactivation_status_changed,
)
def register_presence_router_callbacks(
diff --git a/synapse/python_dependencies.py b/synapse/python_dependencies.py
index 8f48a33936..b40a7bbb76 100644
--- a/synapse/python_dependencies.py
+++ b/synapse/python_dependencies.py
@@ -83,8 +83,8 @@ REQUIREMENTS = [
# ijson 3.1.4 fixes a bug with "." in property names
"ijson>=3.1.4",
"matrix-common~=1.1.0",
- # For runtime introspection of our dependencies
- "packaging~=21.3",
+ # We need packaging.requirements.Requirement, added in 16.1.
+ "packaging>=16.1",
]
CONDITIONAL_REQUIREMENTS = {
diff --git a/synapse/util/check_dependencies.py b/synapse/util/check_dependencies.py
index 3a1f6b3c75..39b0a91db3 100644
--- a/synapse/util/check_dependencies.py
+++ b/synapse/util/check_dependencies.py
@@ -1,3 +1,25 @@
+# Copyright 2022 The Matrix.org Foundation C.I.C.
+#
+# 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.
+#
+
+"""
+This module exposes a single function which checks synapse's dependencies are present
+and correctly versioned. It makes use of `importlib.metadata` to do so. The details
+are a bit murky: there's no easy way to get a map from "extras" to the packages they
+require. But this is probably just symptomatic of Python's package management.
+"""
+
import logging
from typing import Iterable, NamedTuple, Optional
@@ -10,6 +32,8 @@ try:
except ImportError:
import importlib_metadata as metadata # type: ignore[no-redef]
+__all__ = ["check_requirements"]
+
class DependencyException(Exception):
@property
@@ -29,7 +53,17 @@ class DependencyException(Exception):
yield '"' + i + '"'
-EXTRAS = set(metadata.metadata(DISTRIBUTION_NAME).get_all("Provides-Extra"))
+DEV_EXTRAS = {"lint", "mypy", "test", "dev"}
+RUNTIME_EXTRAS = (
+ set(metadata.metadata(DISTRIBUTION_NAME).get_all("Provides-Extra")) - DEV_EXTRAS
+)
+VERSION = metadata.version(DISTRIBUTION_NAME)
+
+
+def _is_dev_dependency(req: Requirement) -> bool:
+ return req.marker is not None and any(
+ req.marker.evaluate({"extra": e}) for e in DEV_EXTRAS
+ )
class Dependency(NamedTuple):
@@ -43,6 +77,9 @@ def _generic_dependencies() -> Iterable[Dependency]:
assert requirements is not None
for raw_requirement in requirements:
req = Requirement(raw_requirement)
+ if _is_dev_dependency(req):
+ continue
+
# https://packaging.pypa.io/en/latest/markers.html#usage notes that
# > Evaluating an extra marker with no environment is an error
# so we pass in a dummy empty extra value here.
@@ -56,6 +93,8 @@ def _dependencies_for_extra(extra: str) -> Iterable[Dependency]:
assert requirements is not None
for raw_requirement in requirements:
req = Requirement(raw_requirement)
+ if _is_dev_dependency(req):
+ continue
# Exclude mandatory deps by only selecting deps needed with this extra.
if (
req.marker is not None
@@ -67,18 +106,26 @@ def _dependencies_for_extra(extra: str) -> Iterable[Dependency]:
def _not_installed(requirement: Requirement, extra: Optional[str] = None) -> str:
if extra:
- return f"Need {requirement.name} for {extra}, but it is not installed"
+ return (
+ f"Synapse {VERSION} needs {requirement.name} for {extra}, "
+ f"but it is not installed"
+ )
else:
- return f"Need {requirement.name}, but it is not installed"
+ return f"Synapse {VERSION} needs {requirement.name}, but it is not installed"
def _incorrect_version(
requirement: Requirement, got: str, extra: Optional[str] = None
) -> str:
if extra:
- return f"Need {requirement} for {extra}, but got {requirement.name}=={got}"
+ return (
+ f"Synapse {VERSION} needs {requirement} for {extra}, "
+ f"but got {requirement.name}=={got}"
+ )
else:
- return f"Need {requirement}, but got {requirement.name}=={got}"
+ return (
+ f"Synapse {VERSION} needs {requirement}, but got {requirement.name}=={got}"
+ )
def check_requirements(extra: Optional[str] = None) -> None:
@@ -100,10 +147,10 @@ def check_requirements(extra: Optional[str] = None) -> None:
# First work out which dependencies are required, and which are optional.
if extra is None:
dependencies = _generic_dependencies()
- elif extra in EXTRAS:
+ elif extra in RUNTIME_EXTRAS:
dependencies = _dependencies_for_extra(extra)
else:
- raise ValueError(f"Synapse does not provide the feature '{extra}'")
+ raise ValueError(f"Synapse {VERSION} does not provide the feature '{extra}'")
deps_unfulfilled = []
errors = []
diff --git a/tests/util/test_check_dependencies.py b/tests/util/test_check_dependencies.py
index 3c07252252..a91c33272f 100644
--- a/tests/util/test_check_dependencies.py
+++ b/tests/util/test_check_dependencies.py
@@ -65,6 +65,23 @@ class TestDependencyChecker(TestCase):
# should not raise
check_requirements()
+ def test_checks_ignore_dev_dependencies(self) -> None:
+ """Bot generic and per-extra checks should ignore dev dependencies."""
+ with patch(
+ "synapse.util.check_dependencies.metadata.requires",
+ return_value=["dummypkg >= 1; extra == 'mypy'"],
+ ), patch("synapse.util.check_dependencies.RUNTIME_EXTRAS", {"cool-extra"}):
+ # We're testing that none of these calls raise.
+ with self.mock_installed_package(None):
+ check_requirements()
+ check_requirements("cool-extra")
+ with self.mock_installed_package(old):
+ check_requirements()
+ check_requirements("cool-extra")
+ with self.mock_installed_package(new):
+ check_requirements()
+ check_requirements("cool-extra")
+
def test_generic_check_of_optional_dependency(self) -> None:
"""Complain if an optional package is old."""
with patch(
@@ -85,11 +102,11 @@ class TestDependencyChecker(TestCase):
with patch(
"synapse.util.check_dependencies.metadata.requires",
return_value=["dummypkg >= 1; extra == 'cool-extra'"],
- ), patch("synapse.util.check_dependencies.EXTRAS", {"cool-extra"}):
+ ), patch("synapse.util.check_dependencies.RUNTIME_EXTRAS", {"cool-extra"}):
with self.mock_installed_package(None):
self.assertRaises(DependencyException, check_requirements, "cool-extra")
with self.mock_installed_package(old):
self.assertRaises(DependencyException, check_requirements, "cool-extra")
with self.mock_installed_package(new):
# should not raise
- check_requirements()
+ check_requirements("cool-extra")
|