diff options
Diffstat (limited to 'Lib/packaging/util.py')
-rw-r--r-- | Lib/packaging/util.py | 1480 |
1 files changed, 1480 insertions, 0 deletions
diff --git a/Lib/packaging/util.py b/Lib/packaging/util.py new file mode 100644 index 0000000..a1f6782 --- /dev/null +++ b/Lib/packaging/util.py @@ -0,0 +1,1480 @@ +"""Miscellaneous utility functions.""" + +import os +import re +import csv +import imp +import sys +import errno +import codecs +import shutil +import string +import hashlib +import posixpath +import subprocess +import sysconfig +from glob import iglob as std_iglob +from fnmatch import fnmatchcase +from inspect import getsource +from configparser import RawConfigParser + +from packaging import logger +from packaging.errors import (PackagingPlatformError, PackagingFileError, + PackagingExecError, InstallationException, + PackagingInternalError) + +__all__ = [ + # file dependencies + 'newer', 'newer_group', + # helpers for commands (dry-run system) + 'execute', 'write_file', + # spawning programs + 'find_executable', 'spawn', + # path manipulation + 'convert_path', 'change_root', + # 2to3 conversion + 'Mixin2to3', 'run_2to3', + # packaging compatibility helpers + 'cfg_to_args', 'generate_setup_py', + 'egginfo_to_distinfo', + 'get_install_method', + # misc + 'ask', 'check_environ', 'encode_multipart', 'resolve_name', + # querying for information TODO move to sysconfig + 'get_compiler_versions', 'get_platform', 'set_platform', + # configuration TODO move to packaging.config + 'get_pypirc_path', 'read_pypirc', 'generate_pypirc', + 'strtobool', 'split_multiline', +] + +_PLATFORM = None +_DEFAULT_INSTALLER = 'packaging' + + +def newer(source, target): + """Tell if the target is newer than the source. + + Returns true if 'source' exists and is more recently modified than + 'target', or if 'source' exists and 'target' doesn't. + + Returns false if both exist and 'target' is the same age or younger + than 'source'. Raise PackagingFileError if 'source' does not exist. + + Note that this test is not very accurate: files created in the same second + will have the same "age". + """ + if not os.path.exists(source): + raise PackagingFileError("file '%s' does not exist" % + os.path.abspath(source)) + if not os.path.exists(target): + return True + + return os.stat(source).st_mtime > os.stat(target).st_mtime + + +def get_platform(): + """Return a string that identifies the current platform. + + By default, will return the value returned by sysconfig.get_platform(), + but it can be changed by calling set_platform(). + """ + global _PLATFORM + if _PLATFORM is None: + _PLATFORM = sysconfig.get_platform() + return _PLATFORM + + +def set_platform(identifier): + """Set the platform string identifier returned by get_platform(). + + Note that this change doesn't impact the value returned by + sysconfig.get_platform(); it is local to packaging. + """ + global _PLATFORM + _PLATFORM = identifier + + +def convert_path(pathname): + """Return 'pathname' as a name that will work on the native filesystem. + + The path is split on '/' and put back together again using the current + directory separator. Needed because filenames in the setup script are + always supplied in Unix style, and have to be converted to the local + convention before we can actually use them in the filesystem. Raises + ValueError on non-Unix-ish systems if 'pathname' either starts or + ends with a slash. + """ + if os.sep == '/': + return pathname + if not pathname: + return pathname + if pathname[0] == '/': + raise ValueError("path '%s' cannot be absolute" % pathname) + if pathname[-1] == '/': + raise ValueError("path '%s' cannot end with '/'" % pathname) + + paths = pathname.split('/') + while os.curdir in paths: + paths.remove(os.curdir) + if not paths: + return os.curdir + return os.path.join(*paths) + + +def change_root(new_root, pathname): + """Return 'pathname' with 'new_root' prepended. + + If 'pathname' is relative, this is equivalent to + os.path.join(new_root,pathname). Otherwise, it requires making 'pathname' + relative and then joining the two, which is tricky on DOS/Windows. + """ + if os.name == 'posix': + if not os.path.isabs(pathname): + return os.path.join(new_root, pathname) + else: + return os.path.join(new_root, pathname[1:]) + + elif os.name == 'nt': + drive, path = os.path.splitdrive(pathname) + if path[0] == '\\': + path = path[1:] + return os.path.join(new_root, path) + + elif os.name == 'os2': + drive, path = os.path.splitdrive(pathname) + if path[0] == os.sep: + path = path[1:] + return os.path.join(new_root, path) + + else: + raise PackagingPlatformError("nothing known about " + "platform '%s'" % os.name) + +_environ_checked = False + + +def check_environ(): + """Ensure that 'os.environ' has all the environment variables needed. + + We guarantee that users can use in config files, command-line options, + etc. Currently this includes: + HOME - user's home directory (Unix only) + PLAT - description of the current platform, including hardware + and OS (see 'get_platform()') + """ + global _environ_checked + if _environ_checked: + return + + if os.name == 'posix' and 'HOME' not in os.environ: + import pwd + os.environ['HOME'] = pwd.getpwuid(os.getuid())[5] + + if 'PLAT' not in os.environ: + os.environ['PLAT'] = sysconfig.get_platform() + + _environ_checked = True + + +# Needed by 'split_quoted()' +_wordchars_re = _squote_re = _dquote_re = None + + +def _init_regex(): + global _wordchars_re, _squote_re, _dquote_re + _wordchars_re = re.compile(r'[^\\\'\"%s ]*' % string.whitespace) + _squote_re = re.compile(r"'(?:[^'\\]|\\.)*'") + _dquote_re = re.compile(r'"(?:[^"\\]|\\.)*"') + + +# TODO replace with shlex.split after testing + +def split_quoted(s): + """Split a string up according to Unix shell-like rules for quotes and + backslashes. + + In short: words are delimited by spaces, as long as those + spaces are not escaped by a backslash, or inside a quoted string. + Single and double quotes are equivalent, and the quote characters can + be backslash-escaped. The backslash is stripped from any two-character + escape sequence, leaving only the escaped character. The quote + characters are stripped from any quoted string. Returns a list of + words. + """ + # This is a nice algorithm for splitting up a single string, since it + # doesn't require character-by-character examination. It was a little + # bit of a brain-bender to get it working right, though... + if _wordchars_re is None: + _init_regex() + + s = s.strip() + words = [] + pos = 0 + + while s: + m = _wordchars_re.match(s, pos) + end = m.end() + if end == len(s): + words.append(s[:end]) + break + + if s[end] in string.whitespace: # unescaped, unquoted whitespace: now + words.append(s[:end]) # we definitely have a word delimiter + s = s[end:].lstrip() + pos = 0 + + elif s[end] == '\\': # preserve whatever is being escaped; + # will become part of the current word + s = s[:end] + s[end + 1:] + pos = end + 1 + + else: + if s[end] == "'": # slurp singly-quoted string + m = _squote_re.match(s, end) + elif s[end] == '"': # slurp doubly-quoted string + m = _dquote_re.match(s, end) + else: + raise RuntimeError("this can't happen " + "(bad char '%c')" % s[end]) + + if m is None: + raise ValueError("bad string (mismatched %s quotes?)" % s[end]) + + beg, end = m.span() + s = s[:beg] + s[beg + 1:end - 1] + s[end:] + pos = m.end() - 2 + + if pos >= len(s): + words.append(s) + break + + return words + + +def split_multiline(value): + """Split a multiline string into a list, excluding blank lines.""" + + return [element for element in + (line.strip() for line in value.split('\n')) + if element] + + +def execute(func, args, msg=None, dry_run=False): + """Perform some action that affects the outside world. + + Some actions (e.g. writing to the filesystem) are special because + they are disabled by the 'dry_run' flag. This method takes care of all + that bureaucracy for you; all you have to do is supply the + function to call and an argument tuple for it (to embody the + "external action" being performed), and an optional message to + print. + """ + if msg is None: + msg = "%s%r" % (func.__name__, args) + if msg[-2:] == ',)': # correct for singleton tuple + msg = msg[0:-2] + ')' + + logger.info(msg) + if not dry_run: + func(*args) + + +def strtobool(val): + """Convert a string representation of truth to a boolean. + + True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values + are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if + 'val' is anything else. + """ + val = val.lower() + if val in ('y', 'yes', 't', 'true', 'on', '1'): + return True + elif val in ('n', 'no', 'f', 'false', 'off', '0'): + return False + else: + raise ValueError("invalid truth value %r" % (val,)) + + +def byte_compile(py_files, optimize=0, force=False, prefix=None, + base_dir=None, dry_run=False, direct=None): + """Byte-compile a collection of Python source files to either .pyc + or .pyo files in a __pycache__ subdirectory. + + 'py_files' is a list of files to compile; any files that don't end in + ".py" are silently skipped. 'optimize' must be one of the following: + 0 - don't optimize (generate .pyc) + 1 - normal optimization (like "python -O") + 2 - extra optimization (like "python -OO") + This function is independent from the running Python's -O or -B options; + it is fully controlled by the parameters passed in. + + If 'force' is true, all files are recompiled regardless of + timestamps. + + The source filename encoded in each bytecode file defaults to the + filenames listed in 'py_files'; you can modify these with 'prefix' and + 'basedir'. 'prefix' is a string that will be stripped off of each + source filename, and 'base_dir' is a directory name that will be + prepended (after 'prefix' is stripped). You can supply either or both + (or neither) of 'prefix' and 'base_dir', as you wish. + + If 'dry_run' is true, doesn't actually do anything that would + affect the filesystem. + + Byte-compilation is either done directly in this interpreter process + with the standard py_compile module, or indirectly by writing a + temporary script and executing it. Normally, you should let + 'byte_compile()' figure out to use direct compilation or not (see + the source for details). The 'direct' flag is used by the script + generated in indirect mode; unless you know what you're doing, leave + it set to None. + """ + # FIXME use compileall + remove direct/indirect shenanigans + + # First, if the caller didn't force us into direct or indirect mode, + # figure out which mode we should be in. We take a conservative + # approach: choose direct mode *only* if the current interpreter is + # in debug mode and optimize is 0. If we're not in debug mode (-O + # or -OO), we don't know which level of optimization this + # interpreter is running with, so we can't do direct + # byte-compilation and be certain that it's the right thing. Thus, + # always compile indirectly if the current interpreter is in either + # optimize mode, or if either optimization level was requested by + # the caller. + if direct is None: + direct = (__debug__ and optimize == 0) + + # "Indirect" byte-compilation: write a temporary script and then + # run it with the appropriate flags. + if not direct: + from tempfile import mkstemp + # XXX use something better than mkstemp + script_fd, script_name = mkstemp(".py") + os.close(script_fd) + script_fd = None + logger.info("writing byte-compilation script '%s'", script_name) + if not dry_run: + if script_fd is not None: + script = os.fdopen(script_fd, "w", encoding='utf-8') + else: + script = open(script_name, "w", encoding='utf-8') + + with script: + script.write("""\ +from packaging.util import byte_compile +files = [ +""") + + # XXX would be nice to write absolute filenames, just for + # safety's sake (script should be more robust in the face of + # chdir'ing before running it). But this requires abspath'ing + # 'prefix' as well, and that breaks the hack in build_lib's + # 'byte_compile()' method that carefully tacks on a trailing + # slash (os.sep really) to make sure the prefix here is "just + # right". This whole prefix business is rather delicate -- the + # problem is that it's really a directory, but I'm treating it + # as a dumb string, so trailing slashes and so forth matter. + + #py_files = map(os.path.abspath, py_files) + #if prefix: + # prefix = os.path.abspath(prefix) + + script.write(",\n".join(map(repr, py_files)) + "]\n") + script.write(""" +byte_compile(files, optimize=%r, force=%r, + prefix=%r, base_dir=%r, + dry_run=False, + direct=True) +""" % (optimize, force, prefix, base_dir)) + + cmd = [sys.executable, script_name] + + env = os.environ.copy() + env['PYTHONPATH'] = os.path.pathsep.join(sys.path) + try: + spawn(cmd, env=env) + finally: + execute(os.remove, (script_name,), "removing %s" % script_name, + dry_run=dry_run) + + # "Direct" byte-compilation: use the py_compile module to compile + # right here, right now. Note that the script generated in indirect + # mode simply calls 'byte_compile()' in direct mode, a weird sort of + # cross-process recursion. Hey, it works! + else: + from py_compile import compile + + for file in py_files: + if file[-3:] != ".py": + # This lets us be lazy and not filter filenames in + # the "install_lib" command. + continue + + # Terminology from the py_compile module: + # cfile - byte-compiled file + # dfile - purported source filename (same as 'file' by default) + # The second argument to cache_from_source forces the extension to + # be .pyc (if true) or .pyo (if false); without it, the extension + # would depend on the calling Python's -O option + cfile = imp.cache_from_source(file, not optimize) + dfile = file + + if prefix: + if file[:len(prefix)] != prefix: + raise ValueError("invalid prefix: filename %r doesn't " + "start with %r" % (file, prefix)) + dfile = dfile[len(prefix):] + if base_dir: + dfile = os.path.join(base_dir, dfile) + + cfile_base = os.path.basename(cfile) + if direct: + if force or newer(file, cfile): + logger.info("byte-compiling %s to %s", file, cfile_base) + if not dry_run: + compile(file, cfile, dfile) + else: + logger.debug("skipping byte-compilation of %s to %s", + file, cfile_base) + + +_RE_VERSION = re.compile('(\d+\.\d+(\.\d+)*)') +_MAC_OS_X_LD_VERSION = re.compile('^@\(#\)PROGRAM:ld ' + 'PROJECT:ld64-((\d+)(\.\d+)*)') + + +def _find_ld_version(): + """Find the ld version. The version scheme differs under Mac OS X.""" + if sys.platform == 'darwin': + return _find_exe_version('ld -v', _MAC_OS_X_LD_VERSION) + else: + return _find_exe_version('ld -v') + + +def _find_exe_version(cmd, pattern=_RE_VERSION): + """Find the version of an executable by running `cmd` in the shell. + + `pattern` is a compiled regular expression. If not provided, defaults + to _RE_VERSION. If the command is not found, or the output does not + match the mattern, returns None. + """ + from subprocess import Popen, PIPE + executable = cmd.split()[0] + if find_executable(executable) is None: + return None + pipe = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE) + try: + stdout, stderr = pipe.communicate() + finally: + pipe.stdout.close() + pipe.stderr.close() + # some commands like ld under MacOS X, will give the + # output in the stderr, rather than stdout. + if stdout != '': + out_string = stdout + else: + out_string = stderr + + result = pattern.search(out_string) + if result is None: + return None + return result.group(1) + + +def get_compiler_versions(): + """Return a tuple providing the versions of gcc, ld and dllwrap + + For each command, if a command is not found, None is returned. + Otherwise a string with the version is returned. + """ + gcc = _find_exe_version('gcc -dumpversion') + ld = _find_ld_version() + dllwrap = _find_exe_version('dllwrap --version') + return gcc, ld, dllwrap + + +def newer_group(sources, target, missing='error'): + """Return true if 'target' is out-of-date with respect to any file + listed in 'sources'. + + In other words, if 'target' exists and is newer + than every file in 'sources', return false; otherwise return true. + 'missing' controls what we do when a source file is missing; the + default ("error") is to blow up with an OSError from inside 'stat()'; + if it is "ignore", we silently drop any missing source files; if it is + "newer", any missing source files make us assume that 'target' is + out-of-date (this is handy in "dry-run" mode: it'll make you pretend to + carry out commands that wouldn't work because inputs are missing, but + that doesn't matter because you're not actually going to run the + commands). + """ + # If the target doesn't even exist, then it's definitely out-of-date. + if not os.path.exists(target): + return True + + # Otherwise we have to find out the hard way: if *any* source file + # is more recent than 'target', then 'target' is out-of-date and + # we can immediately return true. If we fall through to the end + # of the loop, then 'target' is up-to-date and we return false. + target_mtime = os.stat(target).st_mtime + + for source in sources: + if not os.path.exists(source): + if missing == 'error': # blow up when we stat() the file + pass + elif missing == 'ignore': # missing source dropped from + continue # target's dependency list + elif missing == 'newer': # missing source means target is + return True # out-of-date + + if os.stat(source).st_mtime > target_mtime: + return True + + return False + + +def write_file(filename, contents): + """Create *filename* and write *contents* to it. + + *contents* is a sequence of strings without line terminators. + + This functions is not intended to replace the usual with open + write + idiom in all cases, only with Command.execute, which runs depending on + the dry_run argument and also logs its arguments). + """ + with open(filename, "w") as f: + for line in contents: + f.write(line + "\n") + + +def _is_package(path): + return os.path.isdir(path) and os.path.isfile( + os.path.join(path, '__init__.py')) + + +# Code taken from the pip project +def _is_archive_file(name): + archives = ('.zip', '.tar.gz', '.tar.bz2', '.tgz', '.tar') + ext = splitext(name)[1].lower() + return ext in archives + + +def _under(path, root): + # XXX use os.path + path = path.split(os.sep) + root = root.split(os.sep) + if len(root) > len(path): + return False + for pos, part in enumerate(root): + if path[pos] != part: + return False + return True + + +def _package_name(root_path, path): + # Return a dotted package name, given a subpath + if not _under(path, root_path): + raise ValueError('"%s" is not a subpath of "%s"' % (path, root_path)) + return path[len(root_path) + 1:].replace(os.sep, '.') + + +def find_packages(paths=(os.curdir,), exclude=()): + """Return a list all Python packages found recursively within + directories 'paths' + + 'paths' should be supplied as a sequence of "cross-platform" + (i.e. URL-style) path; it will be converted to the appropriate local + path syntax. + + 'exclude' is a sequence of package names to exclude; '*' can be used as + a wildcard in the names, such that 'foo.*' will exclude all subpackages + of 'foo' (but not 'foo' itself). + """ + packages = [] + discarded = [] + + def _discarded(path): + for discard in discarded: + if _under(path, discard): + return True + return False + + for path in paths: + path = convert_path(path) + for root, dirs, files in os.walk(path): + for dir_ in dirs: + fullpath = os.path.join(root, dir_) + if _discarded(fullpath): + continue + # we work only with Python packages + if not _is_package(fullpath): + discarded.append(fullpath) + continue + # see if it's excluded + excluded = False + package_name = _package_name(path, fullpath) + for pattern in exclude: + if fnmatchcase(package_name, pattern): + excluded = True + break + if excluded: + continue + + # adding it to the list + packages.append(package_name) + return packages + + +def resolve_name(name): + """Resolve a name like ``module.object`` to an object and return it. + + This functions supports packages and attributes without depth limitation: + ``package.package.module.class.class.function.attr`` is valid input. + However, looking up builtins is not directly supported: use + ``builtins.name``. + + Raises ImportError if importing the module fails or if one requested + attribute is not found. + """ + if '.' not in name: + # shortcut + __import__(name) + return sys.modules[name] + + # FIXME clean up this code! + parts = name.split('.') + cursor = len(parts) + module_name = parts[:cursor] + ret = '' + + while cursor > 0: + try: + ret = __import__('.'.join(module_name)) + break + except ImportError: + cursor -= 1 + module_name = parts[:cursor] + + if ret == '': + raise ImportError(parts[0]) + + for part in parts[1:]: + try: + ret = getattr(ret, part) + except AttributeError as exc: + raise ImportError(exc) + + return ret + + +def splitext(path): + """Like os.path.splitext, but take off .tar too""" + base, ext = posixpath.splitext(path) + if base.lower().endswith('.tar'): + ext = base[-4:] + ext + base = base[:-4] + return base, ext + + +if sys.platform == 'darwin': + _cfg_target = None + _cfg_target_split = None + + +def spawn(cmd, search_path=True, dry_run=False, env=None): + """Run another program specified as a command list 'cmd' in a new process. + + 'cmd' is just the argument list for the new process, ie. + cmd[0] is the program to run and cmd[1:] are the rest of its arguments. + There is no way to run a program with a name different from that of its + executable. + + If 'search_path' is true (the default), the system's executable + search path will be used to find the program; otherwise, cmd[0] + must be the exact path to the executable. If 'dry_run' is true, + the command will not actually be run. + + If 'env' is given, it's a environment dictionary used for the execution + environment. + + Raise PackagingExecError if running the program fails in any way; just + return on success. + """ + logger.debug('spawn: running %r', cmd) + if dry_run: + logger.debug('dry run, no process actually spawned') + return + if sys.platform == 'darwin': + global _cfg_target, _cfg_target_split + if _cfg_target is None: + _cfg_target = sysconfig.get_config_var( + 'MACOSX_DEPLOYMENT_TARGET') or '' + if _cfg_target: + _cfg_target_split = [int(x) for x in _cfg_target.split('.')] + if _cfg_target: + # ensure that the deployment target of build process is not less + # than that used when the interpreter was built. This ensures + # extension modules are built with correct compatibility values + env = env or os.environ + cur_target = env.get('MACOSX_DEPLOYMENT_TARGET', _cfg_target) + if _cfg_target_split > [int(x) for x in cur_target.split('.')]: + my_msg = ('$MACOSX_DEPLOYMENT_TARGET mismatch: ' + 'now "%s" but "%s" during configure' + % (cur_target, _cfg_target)) + raise PackagingPlatformError(my_msg) + env = dict(env, MACOSX_DEPLOYMENT_TARGET=cur_target) + + exit_status = subprocess.call(cmd, env=env) + if exit_status != 0: + msg = "command %r failed with exit status %d" + raise PackagingExecError(msg % (cmd, exit_status)) + + +def find_executable(executable, path=None): + """Try to find 'executable' in the directories listed in 'path'. + + *path* is a string listing directories separated by 'os.pathsep' and + defaults to os.environ['PATH']. Returns the complete filename or None + if not found. + """ + if path is None: + path = os.environ['PATH'] + paths = path.split(os.pathsep) + base, ext = os.path.splitext(executable) + + if (sys.platform == 'win32' or os.name == 'os2') and (ext != '.exe'): + executable = executable + '.exe' + + if not os.path.isfile(executable): + for p in paths: + f = os.path.join(p, executable) + if os.path.isfile(f): + # the file exists, we have a shot at spawn working + return f + return None + else: + return executable + + +DEFAULT_REPOSITORY = 'http://pypi.python.org/pypi' +DEFAULT_REALM = 'pypi' +DEFAULT_PYPIRC = """\ +[distutils] +index-servers = + pypi + +[pypi] +username:%s +password:%s +""" + + +def get_pypirc_path(): + """Return path to pypirc config file.""" + return os.path.join(os.path.expanduser('~'), '.pypirc') + + +def generate_pypirc(username, password): + """Create a default .pypirc file.""" + rc = get_pypirc_path() + with open(rc, 'w') as f: + f.write(DEFAULT_PYPIRC % (username, password)) + try: + os.chmod(rc, 0o600) + except OSError: + # should do something better here + pass + + +def read_pypirc(repository=DEFAULT_REPOSITORY, realm=DEFAULT_REALM): + """Read the .pypirc file.""" + rc = get_pypirc_path() + if os.path.exists(rc): + config = RawConfigParser() + config.read(rc) + sections = config.sections() + if 'distutils' in sections: + # let's get the list of servers + index_servers = config.get('distutils', 'index-servers') + _servers = [server.strip() for server in + index_servers.split('\n') + if server.strip() != ''] + if _servers == []: + # nothing set, let's try to get the default pypi + if 'pypi' in sections: + _servers = ['pypi'] + else: + # the file is not properly defined, returning + # an empty dict + return {} + for server in _servers: + current = {'server': server} + current['username'] = config.get(server, 'username') + + # optional params + for key, default in (('repository', DEFAULT_REPOSITORY), + ('realm', DEFAULT_REALM), + ('password', None)): + if config.has_option(server, key): + current[key] = config.get(server, key) + else: + current[key] = default + if (current['server'] == repository or + current['repository'] == repository): + return current + elif 'server-login' in sections: + # old format + server = 'server-login' + if config.has_option(server, 'repository'): + repository = config.get(server, 'repository') + else: + repository = DEFAULT_REPOSITORY + + return {'username': config.get(server, 'username'), + 'password': config.get(server, 'password'), + 'repository': repository, + 'server': server, + 'realm': DEFAULT_REALM} + + return {} + + +# utility functions for 2to3 support + +def run_2to3(files, doctests_only=False, fixer_names=None, + options=None, explicit=None): + """ Wrapper function around the refactor() class which + performs the conversions on a list of python files. + Invoke 2to3 on a list of Python files. The files should all come + from the build area, as the modification is done in-place.""" + + #if not files: + # return + + # Make this class local, to delay import of 2to3 + from lib2to3.refactor import get_fixers_from_package, RefactoringTool + fixers = get_fixers_from_package('lib2to3.fixes') + + if fixer_names: + for fixername in fixer_names: + fixers.extend(get_fixers_from_package(fixername)) + r = RefactoringTool(fixers, options=options) + r.refactor(files, write=True, doctests_only=doctests_only) + + +class Mixin2to3: + """ Wrapper class for commands that run 2to3. + To configure 2to3, setup scripts may either change + the class variables, or inherit from this class + to override how 2to3 is invoked. + """ + # list of fixers to run; defaults to all implicit from lib2to3.fixers + fixer_names = None + # dict of options + options = None + # list of extra fixers to invoke + explicit = None + # TODO need a better way to add just one fixer from a package + # TODO need a way to exclude individual fixers + + def run_2to3(self, files, doctests_only=False): + """ Issues a call to util.run_2to3. """ + return run_2to3(files, doctests_only, self.fixer_names, + self.options, self.explicit) + + # TODO provide initialize/finalize_options + + +RICH_GLOB = re.compile(r'\{([^}]*)\}') +_CHECK_RECURSIVE_GLOB = re.compile(r'[^/\\,{]\*\*|\*\*[^/\\,}]') +_CHECK_MISMATCH_SET = re.compile(r'^[^{]*\}|\{[^}]*$') + + +def iglob(path_glob): + """Extended globbing function that supports ** and {opt1,opt2,opt3}.""" + if _CHECK_RECURSIVE_GLOB.search(path_glob): + msg = """invalid glob %r: recursive glob "**" must be used alone""" + raise ValueError(msg % path_glob) + if _CHECK_MISMATCH_SET.search(path_glob): + msg = """invalid glob %r: mismatching set marker '{' or '}'""" + raise ValueError(msg % path_glob) + return _iglob(path_glob) + + +def _iglob(path_glob): + rich_path_glob = RICH_GLOB.split(path_glob, 1) + if len(rich_path_glob) > 1: + assert len(rich_path_glob) == 3, rich_path_glob + prefix, set, suffix = rich_path_glob + for item in set.split(','): + for path in _iglob(''.join((prefix, item, suffix))): + yield path + else: + if '**' not in path_glob: + for item in std_iglob(path_glob): + yield item + else: + prefix, radical = path_glob.split('**', 1) + if prefix == '': + prefix = '.' + if radical == '': + radical = '*' + else: + # we support both + radical = radical.lstrip('/') + radical = radical.lstrip('\\') + for path, dir, files in os.walk(prefix): + path = os.path.normpath(path) + for file in _iglob(os.path.join(path, radical)): + yield file + + +# HOWTO change cfg_to_args +# +# This function has two major constraints: It is copied by inspect.getsource +# in generate_setup_py; it is used in generated setup.py which may be run by +# any Python version supported by distutils2 (2.4-3.3). +# +# * Keep objects like D1_D2_SETUP_ARGS static, i.e. in the function body +# instead of global. +# * If you use a function from another module, update the imports in +# SETUP_TEMPLATE. Use only modules, classes and functions compatible with +# all versions: codecs.open instead of open, RawConfigParser.readfp instead +# of read, standard exceptions instead of Packaging*Error, etc. +# * If you use a function from this module, update the template and +# generate_setup_py. +# +# test_util tests this function and the generated setup.py, but does not test +# that it's compatible with all Python versions. + +def cfg_to_args(path='setup.cfg'): + """Compatibility helper to use setup.cfg in setup.py. + + This functions uses an existing setup.cfg to generate a dictionnary of + keywords that can be used by distutils.core.setup(**kwargs). It is used + by generate_setup_py. + + *file* is the path to the setup.cfg file. If it doesn't exist, + PackagingFileError is raised. + """ + + # XXX ** == needs testing + D1_D2_SETUP_ARGS = {"name": ("metadata",), + "version": ("metadata",), + "author": ("metadata",), + "author_email": ("metadata",), + "maintainer": ("metadata",), + "maintainer_email": ("metadata",), + "url": ("metadata", "home_page"), + "description": ("metadata", "summary"), + "long_description": ("metadata", "description"), + "download-url": ("metadata",), + "classifiers": ("metadata", "classifier"), + "platforms": ("metadata", "platform"), # ** + "license": ("metadata",), + "requires": ("metadata", "requires_dist"), + "provides": ("metadata", "provides_dist"), # ** + "obsoletes": ("metadata", "obsoletes_dist"), # ** + "package_dir": ("files", 'packages_root'), + "packages": ("files",), + "scripts": ("files",), + "py_modules": ("files", "modules"), # ** + } + + MULTI_FIELDS = ("classifiers", + "platforms", + "requires", + "provides", + "obsoletes", + "packages", + "scripts", + "py_modules") + + def has_get_option(config, section, option): + if config.has_option(section, option): + return config.get(section, option) + elif config.has_option(section, option.replace('_', '-')): + return config.get(section, option.replace('_', '-')) + else: + return False + + # The real code starts here + config = RawConfigParser() + f = codecs.open(path, encoding='utf-8') + try: + config.readfp(f) + finally: + f.close() + + kwargs = {} + for arg in D1_D2_SETUP_ARGS: + if len(D1_D2_SETUP_ARGS[arg]) == 2: + # The distutils field name is different than packaging's + section, option = D1_D2_SETUP_ARGS[arg] + + else: + # The distutils field name is the same thant packaging's + section = D1_D2_SETUP_ARGS[arg][0] + option = arg + + in_cfg_value = has_get_option(config, section, option) + if not in_cfg_value: + # There is no such option in the setup.cfg + if arg == 'long_description': + filenames = has_get_option(config, section, 'description-file') + if filenames: + filenames = split_multiline(filenames) + in_cfg_value = [] + for filename in filenames: + fp = codecs.open(filename, encoding='utf-8') + try: + in_cfg_value.append(fp.read()) + finally: + fp.close() + in_cfg_value = '\n\n'.join(in_cfg_value) + else: + continue + + if arg == 'package_dir' and in_cfg_value: + in_cfg_value = {'': in_cfg_value} + + if arg in MULTI_FIELDS: + # support multiline options + in_cfg_value = split_multiline(in_cfg_value) + + kwargs[arg] = in_cfg_value + + return kwargs + + +SETUP_TEMPLATE = """\ +# This script was automatically generated by packaging +import codecs +from distutils.core import setup +try: + from ConfigParser import RawConfigParser +except ImportError: + from configparser import RawConfigParser + + +%(split_multiline)s + +%(cfg_to_args)s + +setup(**cfg_to_args()) +""" + + +def generate_setup_py(): + """Generate a distutils compatible setup.py using an existing setup.cfg. + + Raises a PackagingFileError when a setup.py already exists. + """ + if os.path.exists("setup.py"): + raise PackagingFileError("a setup.py file already exists") + + source = SETUP_TEMPLATE % {'split_multiline': getsource(split_multiline), + 'cfg_to_args': getsource(cfg_to_args)} + with open("setup.py", "w", encoding='utf-8') as fp: + fp.write(source) + + +# Taken from the pip project +# https://github.com/pypa/pip/blob/master/pip/util.py +def ask(message, options): + """Prompt the user with *message*; *options* contains allowed responses.""" + while True: + response = input(message) + response = response.strip().lower() + if response not in options: + print('invalid response:', repr(response)) + print('choose one of', ', '.join(repr(o) for o in options)) + else: + return response + + +def _parse_record_file(record_file): + distinfo, extra_metadata, installed = ({}, [], []) + with open(record_file, 'r') as rfile: + for path in rfile: + path = path.strip() + if path.endswith('egg-info') and os.path.isfile(path): + distinfo_dir = path.replace('egg-info', 'dist-info') + metadata = path + egginfo = path + elif path.endswith('egg-info') and os.path.isdir(path): + distinfo_dir = path.replace('egg-info', 'dist-info') + egginfo = path + for metadata_file in os.listdir(path): + metadata_fpath = os.path.join(path, metadata_file) + if metadata_file == 'PKG-INFO': + metadata = metadata_fpath + else: + extra_metadata.append(metadata_fpath) + elif 'egg-info' in path and os.path.isfile(path): + # skip extra metadata files + continue + else: + installed.append(path) + + distinfo['egginfo'] = egginfo + distinfo['metadata'] = metadata + distinfo['distinfo_dir'] = distinfo_dir + distinfo['installer_path'] = os.path.join(distinfo_dir, 'INSTALLER') + distinfo['metadata_path'] = os.path.join(distinfo_dir, 'METADATA') + distinfo['record_path'] = os.path.join(distinfo_dir, 'RECORD') + distinfo['requested_path'] = os.path.join(distinfo_dir, 'REQUESTED') + installed.extend([distinfo['installer_path'], distinfo['metadata_path']]) + distinfo['installed'] = installed + distinfo['extra_metadata'] = extra_metadata + return distinfo + + +def _write_record_file(record_path, installed_files): + with open(record_path, 'w', encoding='utf-8') as f: + writer = csv.writer(f, delimiter=',', lineterminator=os.linesep, + quotechar='"') + + for fpath in installed_files: + if fpath.endswith('.pyc') or fpath.endswith('.pyo'): + # do not put size and md5 hash, as in PEP-376 + writer.writerow((fpath, '', '')) + else: + hash = hashlib.md5() + with open(fpath, 'rb') as fp: + hash.update(fp.read()) + md5sum = hash.hexdigest() + size = os.path.getsize(fpath) + writer.writerow((fpath, md5sum, size)) + + # add the RECORD file itself + writer.writerow((record_path, '', '')) + return record_path + + +def egginfo_to_distinfo(record_file, installer=_DEFAULT_INSTALLER, + requested=False, remove_egginfo=False): + """Create files and directories required for PEP 376 + + :param record_file: path to RECORD file as produced by setup.py --record + :param installer: installer name + :param requested: True if not installed as a dependency + :param remove_egginfo: delete egginfo dir? + """ + distinfo = _parse_record_file(record_file) + distinfo_dir = distinfo['distinfo_dir'] + if os.path.isdir(distinfo_dir) and not os.path.islink(distinfo_dir): + shutil.rmtree(distinfo_dir) + elif os.path.exists(distinfo_dir): + os.unlink(distinfo_dir) + + os.makedirs(distinfo_dir) + + # copy setuptools extra metadata files + if distinfo['extra_metadata']: + for path in distinfo['extra_metadata']: + shutil.copy2(path, distinfo_dir) + new_path = path.replace('egg-info', 'dist-info') + distinfo['installed'].append(new_path) + + metadata_path = distinfo['metadata_path'] + logger.info('creating %s', metadata_path) + shutil.copy2(distinfo['metadata'], metadata_path) + + installer_path = distinfo['installer_path'] + logger.info('creating %s', installer_path) + with open(installer_path, 'w') as f: + f.write(installer) + + if requested: + requested_path = distinfo['requested_path'] + logger.info('creating %s', requested_path) + open(requested_path, 'wb').close() + distinfo['installed'].append(requested_path) + + record_path = distinfo['record_path'] + logger.info('creating %s', record_path) + _write_record_file(record_path, distinfo['installed']) + + if remove_egginfo: + egginfo = distinfo['egginfo'] + logger.info('removing %s', egginfo) + if os.path.isfile(egginfo): + os.remove(egginfo) + else: + shutil.rmtree(egginfo) + + +def _has_egg_info(srcdir): + if os.path.isdir(srcdir): + for item in os.listdir(srcdir): + full_path = os.path.join(srcdir, item) + if item.endswith('.egg-info') and os.path.isdir(full_path): + logger.debug("Found egg-info directory.") + return True + logger.debug("No egg-info directory found.") + return False + + +def _has_setuptools_text(setup_py): + return _has_text(setup_py, 'setuptools') + + +def _has_distutils_text(setup_py): + return _has_text(setup_py, 'distutils') + + +def _has_text(setup_py, installer): + installer_pattern = re.compile('import {0}|from {0}'.format(installer)) + with open(setup_py, 'r', encoding='utf-8') as setup: + for line in setup: + if re.search(installer_pattern, line): + logger.debug("Found %s text in setup.py.", installer) + return True + logger.debug("No %s text found in setup.py.", installer) + return False + + +def _has_required_metadata(setup_cfg): + config = RawConfigParser() + config.read([setup_cfg], encoding='utf8') + return (config.has_section('metadata') and + 'name' in config.options('metadata') and + 'version' in config.options('metadata')) + + +def _has_pkg_info(srcdir): + pkg_info = os.path.join(srcdir, 'PKG-INFO') + has_pkg_info = os.path.isfile(pkg_info) + if has_pkg_info: + logger.debug("PKG-INFO file found.") + else: + logger.debug("No PKG-INFO file found.") + return has_pkg_info + + +def _has_setup_py(srcdir): + setup_py = os.path.join(srcdir, 'setup.py') + if os.path.isfile(setup_py): + logger.debug('setup.py file found.') + return True + return False + + +def _has_setup_cfg(srcdir): + setup_cfg = os.path.join(srcdir, 'setup.cfg') + if os.path.isfile(setup_cfg): + logger.debug('setup.cfg file found.') + return True + logger.debug("No setup.cfg file found.") + return False + + +def is_setuptools(path): + """Check if the project is based on setuptools. + + :param path: path to source directory containing a setup.py script. + + Return True if the project requires setuptools to install, else False. + """ + srcdir = os.path.abspath(path) + setup_py = os.path.join(srcdir, 'setup.py') + + return _has_setup_py(srcdir) and (_has_egg_info(srcdir) or + _has_setuptools_text(setup_py)) + + +def is_distutils(path): + """Check if the project is based on distutils. + + :param path: path to source directory containing a setup.py script. + + Return True if the project requires distutils to install, else False. + """ + srcdir = os.path.abspath(path) + setup_py = os.path.join(srcdir, 'setup.py') + + return _has_setup_py(srcdir) and (_has_pkg_info(srcdir) or + _has_distutils_text(setup_py)) + + +def is_packaging(path): + """Check if the project is based on packaging + + :param path: path to source directory containing a setup.cfg file. + + Return True if the project has a valid setup.cfg, else False. + """ + srcdir = os.path.abspath(path) + setup_cfg = os.path.join(srcdir, 'setup.cfg') + + return _has_setup_cfg(srcdir) and _has_required_metadata(setup_cfg) + + +def get_install_method(path): + """Check if the project is based on packaging, setuptools, or distutils + + :param path: path to source directory containing a setup.cfg file, + or setup.py. + + Returns a string representing the best install method to use. + """ + if is_packaging(path): + return "packaging" + elif is_setuptools(path): + return "setuptools" + elif is_distutils(path): + return "distutils" + else: + raise InstallationException('Cannot detect install method') + + +# XXX to be replaced by shutil.copytree +def copy_tree(src, dst, preserve_mode=True, preserve_times=True, + preserve_symlinks=False, update=False, dry_run=False): + # FIXME use of this function is why we get spurious logging message on + # stdout when tests run; kill and replace by shutil! + from distutils.file_util import copy_file + + if not dry_run and not os.path.isdir(src): + raise PackagingFileError( + "cannot copy tree '%s': not a directory" % src) + try: + names = os.listdir(src) + except os.error as e: + errstr = e[1] + if dry_run: + names = [] + else: + raise PackagingFileError( + "error listing files in '%s': %s" % (src, errstr)) + + if not dry_run: + _mkpath(dst) + + outputs = [] + + for n in names: + src_name = os.path.join(src, n) + dst_name = os.path.join(dst, n) + + if preserve_symlinks and os.path.islink(src_name): + link_dest = os.readlink(src_name) + logger.info("linking %s -> %s", dst_name, link_dest) + if not dry_run: + os.symlink(link_dest, dst_name) + outputs.append(dst_name) + + elif os.path.isdir(src_name): + outputs.extend( + copy_tree(src_name, dst_name, preserve_mode, + preserve_times, preserve_symlinks, update, + dry_run=dry_run)) + else: + copy_file(src_name, dst_name, preserve_mode, + preserve_times, update, dry_run=dry_run) + outputs.append(dst_name) + + return outputs + +# cache for by mkpath() -- in addition to cheapening redundant calls, +# eliminates redundant "creating /foo/bar/baz" messages in dry-run mode +_path_created = set() + + +# I don't use os.makedirs because a) it's new to Python 1.5.2, and +# b) it blows up if the directory already exists (I want to silently +# succeed in that case). +def _mkpath(name, mode=0o777, dry_run=False): + # Detect a common bug -- name is None + if not isinstance(name, str): + raise PackagingInternalError( + "mkpath: 'name' must be a string (got %r)" % (name,)) + + # XXX what's the better way to handle verbosity? print as we create + # each directory in the path (the current behaviour), or only announce + # the creation of the whole path? (quite easy to do the latter since + # we're not using a recursive algorithm) + + name = os.path.normpath(name) + created_dirs = [] + if os.path.isdir(name) or name == '': + return created_dirs + if os.path.abspath(name) in _path_created: + return created_dirs + + head, tail = os.path.split(name) + tails = [tail] # stack of lone dirs to create + + while head and tail and not os.path.isdir(head): + head, tail = os.path.split(head) + tails.insert(0, tail) # push next higher dir onto stack + + # now 'head' contains the deepest directory that already exists + # (that is, the child of 'head' in 'name' is the highest directory + # that does *not* exist) + for d in tails: + head = os.path.join(head, d) + abs_head = os.path.abspath(head) + + if abs_head in _path_created: + continue + + logger.info("creating %s", head) + if not dry_run: + try: + os.mkdir(head, mode) + except OSError as exc: + if not (exc.errno == errno.EEXIST and os.path.isdir(head)): + raise PackagingFileError( + "could not create '%s': %s" % (head, exc.args[-1])) + created_dirs.append(head) + + _path_created.add(abs_head) + return created_dirs + + +def encode_multipart(fields, files, boundary=None): + """Prepare a multipart HTTP request. + + *fields* is a sequence of (name: str, value: str) elements for regular + form fields, *files* is a sequence of (name: str, filename: str, value: + bytes) elements for data to be uploaded as files. + + Returns (content_type: bytes, body: bytes) ready for http.client.HTTP. + """ + # Taken from http://code.activestate.com/recipes/146306 + + if boundary is None: + boundary = b'--------------GHSKFJDLGDS7543FJKLFHRE75642756743254' + elif not isinstance(boundary, bytes): + raise TypeError('boundary must be bytes, not %r' % type(boundary)) + + l = [] + for key, values in fields: + # handle multiple entries for the same name + if not isinstance(values, (tuple, list)): + values = [values] + + for value in values: + l.extend(( + b'--' + boundary, + ('Content-Disposition: form-data; name="%s"' % + key).encode('utf-8'), + b'', + value.encode('utf-8'))) + + for key, filename, value in files: + l.extend(( + b'--' + boundary, + ('Content-Disposition: form-data; name="%s"; filename="%s"' % + (key, filename)).encode('utf-8'), + b'', + value)) + + l.append(b'--' + boundary + b'--') + l.append(b'') + + body = b'\r\n'.join(l) + content_type = b'multipart/form-data; boundary=' + boundary + return content_type, body |