# -*- coding: utf-8 -*- # Copyright (C) 2012 Anaconda, Inc # SPDX-License-Identifier: BSD-3-Clause from __future__ import absolute_import, division, print_function, unicode_literals from functools import reduce from logging import getLogger import os from os.path import abspath, basename, expanduser, expandvars, join, normcase, split, splitext import re import subprocess from .compat import on_win, string_types from .. import CondaError from ..auxlib.decorators import memoize from .._vendor.toolz import accumulate, concat from distutils.spawn import find_executable try: # Python 3 from urllib.parse import unquote, urlsplit except ImportError: # pragma: no cover # Python 2 from urllib import unquote # NOQA from urlparse import urlsplit # NOQA log = getLogger(__name__) PATH_MATCH_REGEX = ( r"\./" # ./ r"|\.\." # .. r"|~" # ~ r"|/" # / r"|[a-zA-Z]:[/\\]" # drive letter, colon, forward or backslash r"|\\\\" # windows UNC path r"|//" # windows UNC path ) def is_path(value): if '://' in value: return False return re.match(PATH_MATCH_REGEX, value) def expand(path): # if on_win and PY2: # path = ensure_fs_path_encoding(path) return abspath(expanduser(expandvars(path))) def paths_equal(path1, path2): """ Examples: >>> paths_equal('/a/b/c', '/a/b/c/d/..') True """ if on_win: return normcase(abspath(path1)) == normcase(abspath(path2)) else: return abspath(path1) == abspath(path2) @memoize def url_to_path(url): """Convert a file:// URL to a path. Relative file URLs (i.e. `file:relative/path`) are not supported. """ if is_path(url): return url if not url.startswith("file://"): # pragma: no cover raise CondaError("You can only turn absolute file: urls into paths (not %s)" % url) _, netloc, path, _, _ = urlsplit(url) from .url import percent_decode path = percent_decode(path) if netloc not in ('', 'localhost', '127.0.0.1', '::1'): if not netloc.startswith('\\\\'): # The only net location potentially accessible is a Windows UNC path netloc = '//' + netloc else: netloc = '' # Handle Windows drive letters if present if re.match('^/([a-z])[:|]', path, re.I): path = path[1] + ':' + path[3:] return netloc + path def tokenized_startswith(test_iterable, startswith_iterable): return all(t == sw for t, sw in zip(test_iterable, startswith_iterable)) def get_all_directories(files): return sorted(set(tuple(f.split('/')[:-1]) for f in files) - {()}) def get_leaf_directories(files): # type: (List[str]) -> List[str] # give this function a list of files, and it will hand back a list of leaf directories to # pass to os.makedirs() directories = get_all_directories(files) if not directories: return () leaves = [] def _process(x, y): if not tokenized_startswith(y, x): leaves.append(x) return y last = reduce(_process, directories) if not leaves: leaves.append(directories[-1]) elif not tokenized_startswith(last, leaves[-1]): leaves.append(last) return tuple('/'.join(leaf) for leaf in leaves) def explode_directories(child_directories, already_split=False): # get all directories including parents # use already_split=True for the result of get_all_directories() maybe_split = lambda x: x if already_split else x.split('/') return set(concat(accumulate(join, maybe_split(directory)) for directory in child_directories if directory)) def pyc_path(py_path, python_major_minor_version): ''' This must not return backslashes on Windows as that will break tests and leads to an eventual need to make url_to_path return backslashes too and that may end up changing files on disc or to the result of comparisons with the contents of them. ''' pyver_string = python_major_minor_version.replace('.', '') if pyver_string.startswith('2'): return py_path + 'c' else: directory, py_file = split(py_path) basename_root, extension = splitext(py_file) pyc_file = "__pycache__" + '/' + "%s.cpython-%s%sc" % ( basename_root, pyver_string, extension) return "%s%s%s" % (directory, '/', pyc_file) if directory else pyc_file def missing_pyc_files(python_major_minor_version, files): # returns a tuple of tuples, with the inner tuple being the .py file and the missing .pyc file py_files = (f for f in files if f.endswith('.py')) pyc_matches = ((py_file, pyc_path(py_file, python_major_minor_version)) for py_file in py_files) result = tuple(match for match in pyc_matches if match[1] not in files) return result def parse_entry_point_def(ep_definition): cmd_mod, func = ep_definition.rsplit(':', 1) command, module = cmd_mod.rsplit("=", 1) command, module, func = command.strip(), module.strip(), func.strip() return command, module, func def get_python_short_path(python_version=None): if on_win: return "python.exe" if python_version and '.' not in python_version: python_version = '.'.join(python_version) return join("bin", "python%s" % (python_version or '')) def get_python_site_packages_short_path(python_version): if python_version is None: return None elif on_win: return 'Lib/site-packages' else: py_ver = get_major_minor_version(python_version) return 'lib/python%s/site-packages' % py_ver _VERSION_REGEX = re.compile(r"[0-9]+\.[0-9]+") def get_major_minor_version(string, with_dot=True): # returns None if not found, otherwise two digits as a string # should work for # - 3.5.2 # - 27 # - bin/python2.7 # - lib/python34/site-packages/ # the last two are dangers because windows doesn't have version information there assert isinstance(string, string_types) if string.startswith("lib/python"): pythonstr = string.split("/")[1] start = len("python") if len(pythonstr) < start + 2: return None maj_min = pythonstr[start], pythonstr[start+1:] elif string.startswith("bin/python"): pythonstr = string.split("/")[1] start = len("python") if len(pythonstr) < start + 3: return None assert pythonstr[start+1] == "." maj_min = pythonstr[start], pythonstr[start+2:] else: match = _VERSION_REGEX.match(string) if match: version = match.group(0).split(".") maj_min = version[0], version[1] else: digits = "".join([c for c in string if c.isdigit()]) if len(digits) < 2: return None maj_min = digits[0], digits[1:] return ".".join(maj_min) if with_dot else "".join(maj_min) def get_bin_directory_short_path(): return 'Scripts' if on_win else 'bin' def win_path_ok(path): return path.replace('/', '\\') if on_win else path def win_path_double_escape(path): return path.replace('\\', '\\\\') if on_win else path def win_path_backout(path): # replace all backslashes except those escaping spaces # if we pass a file url, something like file://\\unc\path\on\win, make sure # we clean that up too return re.sub(r"(\\(?! ))", r"/", path).replace(':////', '://') def ensure_pad(name, pad="_"): """ Examples: >>> ensure_pad('conda') '_conda_' >>> ensure_pad('_conda') '__conda_' >>> ensure_pad('') '' """ if not name or name[0] == name[-1] == pad: return name else: return "%s%s%s" % (pad, name, pad) def is_private_env_name(env_name): """ Examples: >>> is_private_env_name("_conda") False >>> is_private_env_name("_conda_") True """ return env_name and env_name[0] == env_name[-1] == "_" def is_private_env_path(env_path): """ Examples: >>> is_private_env_path('/some/path/to/envs/_conda_') True >>> is_private_env_path('/not/an/envs_dir/_conda_') False """ if env_path is not None: envs_directory, env_name = split(env_path) if basename(envs_directory) != "envs": return False return is_private_env_name(env_name) return False def right_pad_os_sep(path): return path if path.endswith(os.sep) else path + os.sep def split_filename(path_or_url): dn, fn = split(path_or_url) return (dn or None, fn) if '.' in fn else (path_or_url, None) def get_python_noarch_target_path(source_short_path, target_site_packages_short_path): if source_short_path.startswith('site-packages/'): sp_dir = target_site_packages_short_path return source_short_path.replace('site-packages', sp_dir, 1) elif source_short_path.startswith('python-scripts/'): bin_dir = get_bin_directory_short_path() return source_short_path.replace('python-scripts', bin_dir, 1) else: return source_short_path def win_path_to_unix(path, root_prefix=""): # If the user wishes to drive conda from MSYS2 itself while also having # msys2 packages in their environment this allows the path conversion to # happen relative to the actual shell. The onus is on the user to set # CYGPATH to e.g. /usr/bin/cygpath.exe (this will be translated to e.g. # (C:\msys32\usr\bin\cygpath.exe by MSYS2) to ensure this one is used. if not path: return '' bash = which('bash') if bash: cygpath = os.environ.get('CYGPATH', os.path.join(os.path.dirname(bash), 'cygpath.exe')) else: cygpath = os.environ.get('CYGPATH', 'cygpath.exe') try: path = subprocess.check_output([cygpath, '-up', path]).decode('ascii').split('\n')[0] except Exception as e: log.debug('%r' % e, exc_info=True) # Convert a path or ;-separated string of paths into a unix representation # Does not add cygdrive. If you need that, set root_prefix to "/cygdrive" def _translation(found_path): # NOQA found = found_path.group(1).replace("\\", "/").replace(":", "").replace("//", "/") return root_prefix + "/" + found path_re = '(?|]+[/\\\\]+)*[^:*?"<>|;/\\\\]+?(?![a-zA-Z]:))' # noqa path = re.sub(path_re, _translation, path).replace(";/", ":/") return path def which(executable): return find_executable(executable) def strip_pkg_extension(path): """ Examples: >>> strip_pkg_extension("/path/_license-1.1-py27_1.tar.bz2") ('/path/_license-1.1-py27_1', '.tar.bz2') >>> strip_pkg_extension("/path/_license-1.1-py27_1.conda") ('/path/_license-1.1-py27_1', '.conda') >>> strip_pkg_extension("/path/_license-1.1-py27_1") ('/path/_license-1.1-py27_1', None) """ # NOTE: not using CONDA_TARBALL_EXTENSION_V1 or CONDA_TARBALL_EXTENSION_V2 to comply with # import rules and to avoid a global lookup. if path[-6:] == ".conda": return path[:-6], ".conda" elif path[-8:] == ".tar.bz2": return path[:-8], ".tar.bz2" elif path[-5:] == ".json": return path[:-5], ".json" else: return path, None def is_package_file(path): """ Examples: >>> is_package_file("/path/_license-1.1-py27_1.tar.bz2") True >>> is_package_file("/path/_license-1.1-py27_1.conda") True >>> is_package_file("/path/_license-1.1-py27_1") False """ # NOTE: not using CONDA_TARBALL_EXTENSION_V1 or CONDA_TARBALL_EXTENSION_V2 to comply with # import rules and to avoid a global lookup. return path[-6:] == ".conda" or path[-8:] == ".tar.bz2"