Source code for rez.package_py_utils

# SPDX-License-Identifier: Apache-2.0
# Copyright Contributors to the Rez Project


"""
This sourcefile is intended to be imported in package.py files, in functions
including:

- the special 'preprocess' function;
- early bound functions that use the @early decorator.
"""

# these imports just forward the symbols into this module's namespace
from rez.utils.execution import Popen
from rez.exceptions import InvalidPackageError
from rez.vendor.six import six


basestring = six.string_types[0]


[docs]def expand_requirement(request, paths=None): """Expands a requirement string like 'python-2.*', 'foo-2.*+<*', etc. Wildcards are expanded to the latest version that matches. There is also a special wildcard '**' that will expand to the full version, but it cannot be used in combination with '*'. Wildcards MUST placehold a whole version token, not partial - while 'foo-2.*' is valid, 'foo-2.v*' is not. Wildcards MUST appear at the end of version numbers - while 'foo-1.*.*' is valid, 'foo-1.*.0' is not. It is possible that an expansion will result in an invalid request string (such as 'foo-2+<2'). The appropriate exception will be raised if this happens. Examples: >>> print(expand_requirement('python-2.*')) python-2.7 >>> print(expand_requirement('python==2.**')) python==2.7.12 >>> print(expand_requirement('python<**')) python<3.0.5 Args: request (str): Request to expand, eg 'python-2.*' paths (list of str, optional): paths to search for package families, defaults to `config.packages_path`. Returns: str: Expanded request string. """ if '*' not in request: return request from rez.vendor.version.version import VersionRange from rez.vendor.version.requirement import Requirement from rez.packages import get_latest_package from uuid import uuid4 wildcard_map = {} expanded_versions = {} request_ = request # replace wildcards with valid version tokens that can be replaced again # afterwards. This produces a horrendous, but both valid and temporary, # version string. # while "**" in request_: uid = "_%s_" % uuid4().hex request_ = request_.replace("**", uid, 1) wildcard_map[uid] = "**" while '*' in request_: uid = "_%s_" % uuid4().hex request_ = request_.replace('*', uid, 1) wildcard_map[uid] = '*' # create the requirement, then expand wildcards # req = Requirement(request_, invalid_bound_error=False) def expand_version(version): rank = len(version) wildcard_found = False while version and str(version[-1]) in wildcard_map: token = wildcard_map[str(version[-1])] version = version.trim(len(version) - 1) if token == "**": if wildcard_found: # catches bad syntax '**.*' return None else: wildcard_found = True rank = 0 break wildcard_found = True if not wildcard_found: return None range_ = VersionRange(str(version)) package = get_latest_package(name=req.name, range_=range_, paths=paths) if package is None: return version if rank: return package.version.trim(rank) else: return package.version def visit_version(version): # requirements like 'foo-1' are actually represented internally as # 'foo-1+<1_' - '1_' is the next possible version after '1'. So we have # to detect this case and remap the uid-ified wildcard back here too. # for v, expanded_v in expanded_versions.items(): if version == next(v): return next(expanded_v) version_ = expand_version(version) if version_ is None: return None expanded_versions[version] = version_ return version_ if req.range_ is not None: req.range_.visit_versions(visit_version) result = str(req) # do some cleanup so that long uids aren't left in invalid wildcarded strings for uid, token in wildcard_map.items(): result = result.replace(uid, token) # cast back to a Requirement again, then back to a string. This will catch # bad verison ranges, but will also put OR'd version ranges into the correct # order expanded_req = Requirement(result) return str(expanded_req)
[docs]def expand_requires(*requests): """Create an expanded requirements list. Example: >>> print(expand_requires(["boost-1.*.*"])) ["boost-1.55.0"] >>> print(expand_requires(["boost-1.*"])) ["boost-1.55"] Args: requests (list of str): Requirements to expand. Each value may have trailing wildcards. Returns: List of str: Expanded requirements. """ return [expand_requirement(x) for x in requests]
[docs]def exec_command(attr, cmd): """Runs a subproc to calculate a package attribute. """ import subprocess p = Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) out, err = p.communicate() if p.returncode: from rez.exceptions import InvalidPackageError raise InvalidPackageError( "Error determining package attribute '%s':\n%s" % (attr, err)) return out.strip(), err.strip()
[docs]def exec_python(attr, src, executable="python"): """Runs a python subproc to calculate a package attribute. Args: attr (str): Name of package attribute being created. src (list of str): Python code to execute, will be converted into semicolon-delimited single line of code. Returns: str: Output of python process. """ import subprocess if isinstance(src, basestring): src = [src] p = Popen([executable, "-c", "; ".join(src)], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) out, err = p.communicate() if p.returncode: from rez.exceptions import InvalidPackageError raise InvalidPackageError( "Error determining package attribute '%s':\n%s" % (attr, err)) return out.strip()
[docs]def find_site_python(module_name, paths=None): """Find the rez native python package that contains the given module. This function is used by python 'native' rez installers to find the native rez python package that represents the python installation that this module is installed into. Note: This function is dependent on the behavior found in the python '_native' package found in the 'rez-recipes' repository. Specifically, it expects to find a python package with a '_site_paths' list attribute listing the site directories associated with the python installation. Args: module_name (str): Target python module. paths (list of str, optional): paths to search for packages, defaults to `config.packages_path`. Returns: `Package`: Native python package containing the named module. """ from rez.packages import iter_packages import subprocess import ast import os py_cmd = 'import {x}; print({x}.__path__)'.format(x=module_name) p = Popen( ["python", "-c", py_cmd], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) out, err = p.communicate() if p.returncode: raise InvalidPackageError( "Failed to find installed python module '%s':\n%s" % (module_name, err)) module_paths = ast.literal_eval(out.strip()) def issubdir(path, parent_path): return path.startswith(parent_path + os.sep) for package in iter_packages("python", paths=paths): if not hasattr(package, "_site_paths"): continue contained = True for module_path in module_paths: if not any(issubdir(module_path, x) for x in package._site_paths): contained = False if contained: return package raise InvalidPackageError( "Failed to find python installation containing the module '%s'. Has " "python been installed as a rez package?" % module_name)