Source code

Revision control

Copy as Markdown

Other Tools

# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import os
from pathlib import Path
from packaging.requirements import Requirement
THUNDERBIRD_PYPI_ERROR = """
Thunderbird requirements definitions cannot include PyPI packages.
""".strip()
class PthSpecifier:
def __init__(self, path: str):
self.path = path
class PypiSpecifier:
def __init__(self, requirement):
self.requirement = requirement
class PypiOptionalSpecifier(PypiSpecifier):
def __init__(self, repercussion, requirement):
super().__init__(requirement)
self.repercussion = repercussion
class MachEnvRequirements:
"""Requirements associated with a "site dependency manifest", as
defined in "python/sites/".
Represents the dependencies of a site. The source files consist
of colon-delimited fields. The first field
specifies the action. The remaining fields are arguments to that
action. The following actions are supported:
pth -- Adds the path given as argument to "mach.pth" under
the virtualenv's site packages directory.
pypi -- Fetch the package, plus dependencies, from PyPI.
pypi-optional -- Attempt to install the package and dependencies from PyPI.
Continue using the site, even if the package could not be installed.
packages.txt -- Denotes that the specified path is a child manifest. It
will be read and processed as if its contents were concatenated
into the manifest being read.
thunderbird-packages.txt -- Denotes a Thunderbird child manifest.
Thunderbird child manifests are only activated when working on Thunderbird,
and they can cannot have "pypi" or "pypi-optional" entries.
"""
def __init__(self):
self.requirements_paths = []
self.pth_requirements = []
self.pypi_requirements = []
self.pypi_optional_requirements = []
self.vendored_requirements = []
def pths_as_absolute(self, topsrcdir: str):
return [
os.path.normcase(Path(topsrcdir) / pth.path)
for pth in (self.pth_requirements + self.vendored_requirements)
]
@classmethod
def from_requirements_definition(
cls,
topsrcdir: str,
is_thunderbird,
only_strict_requirements,
requirements_definition,
):
requirements = cls()
_parse_mach_env_requirements(
requirements,
Path(requirements_definition),
Path(topsrcdir),
is_thunderbird,
only_strict_requirements,
)
return requirements
def _parse_mach_env_requirements(
requirements_output,
root_requirements_path: Path,
topsrcdir: Path,
is_thunderbird,
only_strict_requirements,
):
def _parse_requirements_line(
current_requirements_path: Path, line, line_number, is_thunderbird_packages_txt
):
line = line.strip()
if not line or line.startswith("#"):
return
action, params = line.rstrip().split(":", maxsplit=1)
if action == "pth":
path = topsrcdir / params
if not path.exists():
# In sparse checkouts, not all paths will be populated.
return
requirements_output.pth_requirements.append(PthSpecifier(params))
elif action == "vendored":
requirements_output.vendored_requirements.append(PthSpecifier(params))
elif action == "packages.txt":
_parse_requirements_definition_file(
topsrcdir / params,
is_thunderbird_packages_txt,
)
elif action == "pypi":
if is_thunderbird_packages_txt:
raise Exception(THUNDERBIRD_PYPI_ERROR)
requirements_output.pypi_requirements.append(
PypiSpecifier(
_parse_package_specifier(params, only_strict_requirements)
)
)
elif action == "pypi-optional":
if is_thunderbird_packages_txt:
raise Exception(THUNDERBIRD_PYPI_ERROR)
if len(params.split(":", maxsplit=1)) != 2:
raise Exception(
"Expected pypi-optional package to have a repercussion "
'description in the format "package:fallback explanation", '
'found "{}"'.format(params)
)
raw_requirement, repercussion = params.split(":")
requirements_output.pypi_optional_requirements.append(
PypiOptionalSpecifier(
repercussion,
_parse_package_specifier(raw_requirement, only_strict_requirements),
)
)
elif action == "thunderbird-packages.txt":
if is_thunderbird:
_parse_requirements_definition_file(
topsrcdir / params, is_thunderbird_packages_txt=True
)
else:
raise Exception("Unknown requirements definition action: %s" % action)
def _parse_requirements_definition_file(
requirements_path: Path, is_thunderbird_packages_txt
):
"""Parse requirements file into list of requirements"""
if not requirements_path.is_file():
raise Exception(f'Missing requirements file at "{requirements_path}"')
requirements_output.requirements_paths.append(str(requirements_path))
with open(requirements_path, "r") as requirements_file:
lines = [line for line in requirements_file]
for number, line in enumerate(lines, start=1):
_parse_requirements_line(
requirements_path, line, number, is_thunderbird_packages_txt
)
_parse_requirements_definition_file(root_requirements_path, False)
class UnexpectedFlexibleRequirementException(Exception):
def __init__(self, raw_requirement):
self.raw_requirement = raw_requirement
def _parse_package_specifier(raw_requirement, only_strict_requirements):
requirement = Requirement(raw_requirement)
if only_strict_requirements and [
s for s in requirement.specifier if s.operator != "=="
]:
raise UnexpectedFlexibleRequirementException(raw_requirement)
return requirement