forked from MirrorHub/synapse
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
This commit is contained in:
parent
ae8a616b49
commit
cea1b58c4a
3 changed files with 74 additions and 9 deletions
1
changelog.d/12129.misc
Normal file
1
changelog.d/12129.misc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Inspect application dependencies using `importlib.metadata` or its backport.
|
|
@ -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
|
import logging
|
||||||
from typing import Iterable, NamedTuple, Optional
|
from typing import Iterable, NamedTuple, Optional
|
||||||
|
|
||||||
|
@ -10,6 +32,8 @@ try:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
import importlib_metadata as metadata # type: ignore[no-redef]
|
import importlib_metadata as metadata # type: ignore[no-redef]
|
||||||
|
|
||||||
|
__all__ = ["check_requirements"]
|
||||||
|
|
||||||
|
|
||||||
class DependencyException(Exception):
|
class DependencyException(Exception):
|
||||||
@property
|
@property
|
||||||
|
@ -29,7 +53,17 @@ class DependencyException(Exception):
|
||||||
yield '"' + i + '"'
|
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):
|
class Dependency(NamedTuple):
|
||||||
|
@ -43,6 +77,9 @@ def _generic_dependencies() -> Iterable[Dependency]:
|
||||||
assert requirements is not None
|
assert requirements is not None
|
||||||
for raw_requirement in requirements:
|
for raw_requirement in requirements:
|
||||||
req = Requirement(raw_requirement)
|
req = Requirement(raw_requirement)
|
||||||
|
if _is_dev_dependency(req):
|
||||||
|
continue
|
||||||
|
|
||||||
# https://packaging.pypa.io/en/latest/markers.html#usage notes that
|
# https://packaging.pypa.io/en/latest/markers.html#usage notes that
|
||||||
# > Evaluating an extra marker with no environment is an error
|
# > Evaluating an extra marker with no environment is an error
|
||||||
# so we pass in a dummy empty extra value here.
|
# 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
|
assert requirements is not None
|
||||||
for raw_requirement in requirements:
|
for raw_requirement in requirements:
|
||||||
req = Requirement(raw_requirement)
|
req = Requirement(raw_requirement)
|
||||||
|
if _is_dev_dependency(req):
|
||||||
|
continue
|
||||||
# Exclude mandatory deps by only selecting deps needed with this extra.
|
# Exclude mandatory deps by only selecting deps needed with this extra.
|
||||||
if (
|
if (
|
||||||
req.marker is not None
|
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:
|
def _not_installed(requirement: Requirement, extra: Optional[str] = None) -> str:
|
||||||
if extra:
|
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:
|
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(
|
def _incorrect_version(
|
||||||
requirement: Requirement, got: str, extra: Optional[str] = None
|
requirement: Requirement, got: str, extra: Optional[str] = None
|
||||||
) -> str:
|
) -> str:
|
||||||
if extra:
|
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:
|
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:
|
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.
|
# First work out which dependencies are required, and which are optional.
|
||||||
if extra is None:
|
if extra is None:
|
||||||
dependencies = _generic_dependencies()
|
dependencies = _generic_dependencies()
|
||||||
elif extra in EXTRAS:
|
elif extra in RUNTIME_EXTRAS:
|
||||||
dependencies = _dependencies_for_extra(extra)
|
dependencies = _dependencies_for_extra(extra)
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Synapse does not provide the feature '{extra}'")
|
raise ValueError(f"Synapse {VERSION} does not provide the feature '{extra}'")
|
||||||
|
|
||||||
deps_unfulfilled = []
|
deps_unfulfilled = []
|
||||||
errors = []
|
errors = []
|
||||||
|
|
|
@ -65,6 +65,23 @@ class TestDependencyChecker(TestCase):
|
||||||
# should not raise
|
# should not raise
|
||||||
check_requirements()
|
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:
|
def test_generic_check_of_optional_dependency(self) -> None:
|
||||||
"""Complain if an optional package is old."""
|
"""Complain if an optional package is old."""
|
||||||
with patch(
|
with patch(
|
||||||
|
@ -85,11 +102,11 @@ class TestDependencyChecker(TestCase):
|
||||||
with patch(
|
with patch(
|
||||||
"synapse.util.check_dependencies.metadata.requires",
|
"synapse.util.check_dependencies.metadata.requires",
|
||||||
return_value=["dummypkg >= 1; extra == 'cool-extra'"],
|
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):
|
with self.mock_installed_package(None):
|
||||||
self.assertRaises(DependencyException, check_requirements, "cool-extra")
|
self.assertRaises(DependencyException, check_requirements, "cool-extra")
|
||||||
with self.mock_installed_package(old):
|
with self.mock_installed_package(old):
|
||||||
self.assertRaises(DependencyException, check_requirements, "cool-extra")
|
self.assertRaises(DependencyException, check_requirements, "cool-extra")
|
||||||
with self.mock_installed_package(new):
|
with self.mock_installed_package(new):
|
||||||
# should not raise
|
# should not raise
|
||||||
check_requirements()
|
check_requirements("cool-extra")
|
||||||
|
|
Loading…
Reference in a new issue