Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit cea1b58

Browse files
author
David Robertson
authored
Don't impose version checks on dev extras at runtime (#12129)
* Fix incorrect argument in test case * Add copyright header * Docstring and __all__ * Exclude dev depenencies * Use changelog from #12088 * Include version in error messages This will hopefully distinguish between the version of the source code and the version of the distribution package that is installed. * Linter script is your friend
1 parent ae8a616 commit cea1b58

File tree

3 files changed

+74
-9
lines changed

3 files changed

+74
-9
lines changed

changelog.d/12129.misc

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Inspect application dependencies using `importlib.metadata` or its backport.

synapse/util/check_dependencies.py

+54-7
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,25 @@
1+
# Copyright 2022 The Matrix.org Foundation C.I.C.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
16+
"""
17+
This module exposes a single function which checks synapse's dependencies are present
18+
and correctly versioned. It makes use of `importlib.metadata` to do so. The details
19+
are a bit murky: there's no easy way to get a map from "extras" to the packages they
20+
require. But this is probably just symptomatic of Python's package management.
21+
"""
22+
123
import logging
224
from typing import Iterable, NamedTuple, Optional
325

@@ -10,6 +32,8 @@
1032
except ImportError:
1133
import importlib_metadata as metadata # type: ignore[no-redef]
1234

35+
__all__ = ["check_requirements"]
36+
1337

1438
class DependencyException(Exception):
1539
@property
@@ -29,7 +53,17 @@ def dependencies(self) -> Iterable[str]:
2953
yield '"' + i + '"'
3054

3155

32-
EXTRAS = set(metadata.metadata(DISTRIBUTION_NAME).get_all("Provides-Extra"))
56+
DEV_EXTRAS = {"lint", "mypy", "test", "dev"}
57+
RUNTIME_EXTRAS = (
58+
set(metadata.metadata(DISTRIBUTION_NAME).get_all("Provides-Extra")) - DEV_EXTRAS
59+
)
60+
VERSION = metadata.version(DISTRIBUTION_NAME)
61+
62+
63+
def _is_dev_dependency(req: Requirement) -> bool:
64+
return req.marker is not None and any(
65+
req.marker.evaluate({"extra": e}) for e in DEV_EXTRAS
66+
)
3367

3468

3569
class Dependency(NamedTuple):
@@ -43,6 +77,9 @@ def _generic_dependencies() -> Iterable[Dependency]:
4377
assert requirements is not None
4478
for raw_requirement in requirements:
4579
req = Requirement(raw_requirement)
80+
if _is_dev_dependency(req):
81+
continue
82+
4683
# https://packaging.pypa.io/en/latest/markers.html#usage notes that
4784
# > Evaluating an extra marker with no environment is an error
4885
# so we pass in a dummy empty extra value here.
@@ -56,6 +93,8 @@ def _dependencies_for_extra(extra: str) -> Iterable[Dependency]:
5693
assert requirements is not None
5794
for raw_requirement in requirements:
5895
req = Requirement(raw_requirement)
96+
if _is_dev_dependency(req):
97+
continue
5998
# Exclude mandatory deps by only selecting deps needed with this extra.
6099
if (
61100
req.marker is not None
@@ -67,18 +106,26 @@ def _dependencies_for_extra(extra: str) -> Iterable[Dependency]:
67106

68107
def _not_installed(requirement: Requirement, extra: Optional[str] = None) -> str:
69108
if extra:
70-
return f"Need {requirement.name} for {extra}, but it is not installed"
109+
return (
110+
f"Synapse {VERSION} needs {requirement.name} for {extra}, "
111+
f"but it is not installed"
112+
)
71113
else:
72-
return f"Need {requirement.name}, but it is not installed"
114+
return f"Synapse {VERSION} needs {requirement.name}, but it is not installed"
73115

74116

75117
def _incorrect_version(
76118
requirement: Requirement, got: str, extra: Optional[str] = None
77119
) -> str:
78120
if extra:
79-
return f"Need {requirement} for {extra}, but got {requirement.name}=={got}"
121+
return (
122+
f"Synapse {VERSION} needs {requirement} for {extra}, "
123+
f"but got {requirement.name}=={got}"
124+
)
80125
else:
81-
return f"Need {requirement}, but got {requirement.name}=={got}"
126+
return (
127+
f"Synapse {VERSION} needs {requirement}, but got {requirement.name}=={got}"
128+
)
82129

83130

84131
def check_requirements(extra: Optional[str] = None) -> None:
@@ -100,10 +147,10 @@ def check_requirements(extra: Optional[str] = None) -> None:
100147
# First work out which dependencies are required, and which are optional.
101148
if extra is None:
102149
dependencies = _generic_dependencies()
103-
elif extra in EXTRAS:
150+
elif extra in RUNTIME_EXTRAS:
104151
dependencies = _dependencies_for_extra(extra)
105152
else:
106-
raise ValueError(f"Synapse does not provide the feature '{extra}'")
153+
raise ValueError(f"Synapse {VERSION} does not provide the feature '{extra}'")
107154

108155
deps_unfulfilled = []
109156
errors = []

tests/util/test_check_dependencies.py

+19-2
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,23 @@ def test_mandatory_dependency(self) -> None:
6565
# should not raise
6666
check_requirements()
6767

68+
def test_checks_ignore_dev_dependencies(self) -> None:
69+
"""Bot generic and per-extra checks should ignore dev dependencies."""
70+
with patch(
71+
"synapse.util.check_dependencies.metadata.requires",
72+
return_value=["dummypkg >= 1; extra == 'mypy'"],
73+
), patch("synapse.util.check_dependencies.RUNTIME_EXTRAS", {"cool-extra"}):
74+
# We're testing that none of these calls raise.
75+
with self.mock_installed_package(None):
76+
check_requirements()
77+
check_requirements("cool-extra")
78+
with self.mock_installed_package(old):
79+
check_requirements()
80+
check_requirements("cool-extra")
81+
with self.mock_installed_package(new):
82+
check_requirements()
83+
check_requirements("cool-extra")
84+
6885
def test_generic_check_of_optional_dependency(self) -> None:
6986
"""Complain if an optional package is old."""
7087
with patch(
@@ -85,11 +102,11 @@ def test_check_for_extra_dependencies(self) -> None:
85102
with patch(
86103
"synapse.util.check_dependencies.metadata.requires",
87104
return_value=["dummypkg >= 1; extra == 'cool-extra'"],
88-
), patch("synapse.util.check_dependencies.EXTRAS", {"cool-extra"}):
105+
), patch("synapse.util.check_dependencies.RUNTIME_EXTRAS", {"cool-extra"}):
89106
with self.mock_installed_package(None):
90107
self.assertRaises(DependencyException, check_requirements, "cool-extra")
91108
with self.mock_installed_package(old):
92109
self.assertRaises(DependencyException, check_requirements, "cool-extra")
93110
with self.mock_installed_package(new):
94111
# should not raise
95-
check_requirements()
112+
check_requirements("cool-extra")

0 commit comments

Comments
 (0)