128 lines
		
	
	
		
			4.4 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			128 lines
		
	
	
		
			4.4 KiB
		
	
	
	
		
			Python
		
	
	
| import logging
 | |
| from typing import Iterable, NamedTuple, Optional
 | |
| 
 | |
| from packaging.requirements import Requirement
 | |
| 
 | |
| DISTRIBUTION_NAME = "matrix-synapse"
 | |
| 
 | |
| try:
 | |
|     from importlib import metadata
 | |
| except ImportError:
 | |
|     import importlib_metadata as metadata  # type: ignore[no-redef]
 | |
| 
 | |
| 
 | |
| class DependencyException(Exception):
 | |
|     @property
 | |
|     def message(self) -> str:
 | |
|         return "\n".join(
 | |
|             [
 | |
|                 "Missing Requirements: %s" % (", ".join(self.dependencies),),
 | |
|                 "To install run:",
 | |
|                 "    pip install --upgrade --force %s" % (" ".join(self.dependencies),),
 | |
|                 "",
 | |
|             ]
 | |
|         )
 | |
| 
 | |
|     @property
 | |
|     def dependencies(self) -> Iterable[str]:
 | |
|         for i in self.args[0]:
 | |
|             yield '"' + i + '"'
 | |
| 
 | |
| 
 | |
| EXTRAS = set(metadata.metadata(DISTRIBUTION_NAME).get_all("Provides-Extra"))
 | |
| 
 | |
| 
 | |
| class Dependency(NamedTuple):
 | |
|     requirement: Requirement
 | |
|     must_be_installed: bool
 | |
| 
 | |
| 
 | |
| def _generic_dependencies() -> Iterable[Dependency]:
 | |
|     """Yield pairs (requirement, must_be_installed)."""
 | |
|     requirements = metadata.requires(DISTRIBUTION_NAME)
 | |
|     assert requirements is not None
 | |
|     for raw_requirement in requirements:
 | |
|         req = Requirement(raw_requirement)
 | |
|         # 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.
 | |
|         must_be_installed = req.marker is None or req.marker.evaluate({"extra": ""})
 | |
|         yield Dependency(req, must_be_installed)
 | |
| 
 | |
| 
 | |
| def _dependencies_for_extra(extra: str) -> Iterable[Dependency]:
 | |
|     """Yield additional dependencies needed for a given `extra`."""
 | |
|     requirements = metadata.requires(DISTRIBUTION_NAME)
 | |
|     assert requirements is not None
 | |
|     for raw_requirement in requirements:
 | |
|         req = Requirement(raw_requirement)
 | |
|         # Exclude mandatory deps by only selecting deps needed with this extra.
 | |
|         if (
 | |
|             req.marker is not None
 | |
|             and req.marker.evaluate({"extra": extra})
 | |
|             and not req.marker.evaluate({"extra": ""})
 | |
|         ):
 | |
|             yield Dependency(req, True)
 | |
| 
 | |
| 
 | |
| def _not_installed(requirement: Requirement, extra: Optional[str] = None) -> str:
 | |
|     if extra:
 | |
|         return f"Need {requirement.name} for {extra}, but it is not installed"
 | |
|     else:
 | |
|         return f"Need {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}"
 | |
|     else:
 | |
|         return f"Need {requirement}, but got {requirement.name}=={got}"
 | |
| 
 | |
| 
 | |
| def check_requirements(extra: Optional[str] = None) -> None:
 | |
|     """Check Synapse's dependencies are present and correctly versioned.
 | |
| 
 | |
|     If provided, `extra` must be the name of an pacakging extra (e.g. "saml2" in
 | |
|     `pip install matrix-synapse[saml2]`).
 | |
| 
 | |
|     If `extra` is None, this function checks that
 | |
|     - all mandatory dependencies are installed and correctly versioned, and
 | |
|     - each optional dependency that's installed is correctly versioned.
 | |
| 
 | |
|     If `extra` is not None, this function checks that
 | |
|     - the dependencies needed for that extra are installed and correctly versioned.
 | |
| 
 | |
|     :raises DependencyException: if a dependency is missing or incorrectly versioned.
 | |
|     :raises ValueError: if this extra does not exist.
 | |
|     """
 | |
|     # First work out which dependencies are required, and which are optional.
 | |
|     if extra is None:
 | |
|         dependencies = _generic_dependencies()
 | |
|     elif extra in EXTRAS:
 | |
|         dependencies = _dependencies_for_extra(extra)
 | |
|     else:
 | |
|         raise ValueError(f"Synapse does not provide the feature '{extra}'")
 | |
| 
 | |
|     deps_unfulfilled = []
 | |
|     errors = []
 | |
| 
 | |
|     for (requirement, must_be_installed) in dependencies:
 | |
|         try:
 | |
|             dist: metadata.Distribution = metadata.distribution(requirement.name)
 | |
|         except metadata.PackageNotFoundError:
 | |
|             if must_be_installed:
 | |
|                 deps_unfulfilled.append(requirement.name)
 | |
|                 errors.append(_not_installed(requirement, extra))
 | |
|         else:
 | |
|             if not requirement.specifier.contains(dist.version):
 | |
|                 deps_unfulfilled.append(requirement.name)
 | |
|                 errors.append(_incorrect_version(requirement, dist.version, extra))
 | |
| 
 | |
|     if deps_unfulfilled:
 | |
|         for err in errors:
 | |
|             logging.error(err)
 | |
| 
 | |
|         raise DependencyException(deps_unfulfilled)
 |