Éric Araujo | 35a4d01 | 2011-06-04 22:24:59 +0200 | [diff] [blame] | 1 | """Miscellaneous utility functions.""" |
| 2 | |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 3 | import os |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 4 | import re |
Éric Araujo | 35a4d01 | 2011-06-04 22:24:59 +0200 | [diff] [blame] | 5 | import csv |
Éric Araujo | a29e4f6 | 2011-10-08 04:09:15 +0200 | [diff] [blame] | 6 | import imp |
Éric Araujo | 35a4d01 | 2011-06-04 22:24:59 +0200 | [diff] [blame] | 7 | import sys |
| 8 | import errno |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 9 | import shutil |
| 10 | import string |
Éric Araujo | 35a4d01 | 2011-06-04 22:24:59 +0200 | [diff] [blame] | 11 | import hashlib |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 12 | import posixpath |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 13 | import subprocess |
Éric Araujo | 35a4d01 | 2011-06-04 22:24:59 +0200 | [diff] [blame] | 14 | import sysconfig |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 15 | from glob import iglob as std_iglob |
| 16 | from fnmatch import fnmatchcase |
| 17 | from inspect import getsource |
| 18 | from configparser import RawConfigParser |
| 19 | |
| 20 | from packaging import logger |
| 21 | from packaging.errors import (PackagingPlatformError, PackagingFileError, |
| 22 | PackagingByteCompileError, PackagingExecError, |
| 23 | InstallationException, PackagingInternalError) |
| 24 | |
Éric Araujo | 95fc53f | 2011-09-01 05:11:29 +0200 | [diff] [blame] | 25 | __all__ = [ |
| 26 | # file dependencies |
| 27 | 'newer', 'newer_group', |
| 28 | # helpers for commands (dry-run system) |
| 29 | 'execute', 'write_file', |
| 30 | # spawning programs |
| 31 | 'find_executable', 'spawn', |
| 32 | # path manipulation |
| 33 | 'convert_path', 'change_root', |
| 34 | # 2to3 conversion |
| 35 | 'Mixin2to3', 'run_2to3', |
| 36 | # packaging compatibility helpers |
| 37 | 'cfg_to_args', 'generate_setup_py', |
| 38 | 'egginfo_to_distinfo', |
| 39 | 'get_install_method', |
| 40 | # misc |
| 41 | 'ask', 'check_environ', 'encode_multipart', 'resolve_name', |
| 42 | # querying for information TODO move to sysconfig |
| 43 | 'get_compiler_versions', 'get_platform', 'set_platform', |
| 44 | # configuration TODO move to packaging.config |
| 45 | 'get_pypirc_path', 'read_pypirc', 'generate_pypirc', |
| 46 | 'strtobool', 'split_multiline', |
| 47 | ] |
| 48 | |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 49 | _PLATFORM = None |
| 50 | _DEFAULT_INSTALLER = 'packaging' |
| 51 | |
| 52 | |
| 53 | def newer(source, target): |
| 54 | """Tell if the target is newer than the source. |
| 55 | |
| 56 | Returns true if 'source' exists and is more recently modified than |
| 57 | 'target', or if 'source' exists and 'target' doesn't. |
| 58 | |
| 59 | Returns false if both exist and 'target' is the same age or younger |
| 60 | than 'source'. Raise PackagingFileError if 'source' does not exist. |
| 61 | |
| 62 | Note that this test is not very accurate: files created in the same second |
| 63 | will have the same "age". |
| 64 | """ |
| 65 | if not os.path.exists(source): |
| 66 | raise PackagingFileError("file '%s' does not exist" % |
| 67 | os.path.abspath(source)) |
| 68 | if not os.path.exists(target): |
| 69 | return True |
| 70 | |
| 71 | return os.stat(source).st_mtime > os.stat(target).st_mtime |
| 72 | |
| 73 | |
| 74 | def get_platform(): |
| 75 | """Return a string that identifies the current platform. |
| 76 | |
| 77 | By default, will return the value returned by sysconfig.get_platform(), |
| 78 | but it can be changed by calling set_platform(). |
| 79 | """ |
| 80 | global _PLATFORM |
| 81 | if _PLATFORM is None: |
| 82 | _PLATFORM = sysconfig.get_platform() |
| 83 | return _PLATFORM |
| 84 | |
| 85 | |
| 86 | def set_platform(identifier): |
| 87 | """Set the platform string identifier returned by get_platform(). |
| 88 | |
| 89 | Note that this change doesn't impact the value returned by |
| 90 | sysconfig.get_platform(); it is local to packaging. |
| 91 | """ |
| 92 | global _PLATFORM |
| 93 | _PLATFORM = identifier |
| 94 | |
| 95 | |
| 96 | def convert_path(pathname): |
| 97 | """Return 'pathname' as a name that will work on the native filesystem. |
| 98 | |
| 99 | The path is split on '/' and put back together again using the current |
| 100 | directory separator. Needed because filenames in the setup script are |
| 101 | always supplied in Unix style, and have to be converted to the local |
| 102 | convention before we can actually use them in the filesystem. Raises |
| 103 | ValueError on non-Unix-ish systems if 'pathname' either starts or |
| 104 | ends with a slash. |
| 105 | """ |
| 106 | if os.sep == '/': |
| 107 | return pathname |
| 108 | if not pathname: |
| 109 | return pathname |
| 110 | if pathname[0] == '/': |
| 111 | raise ValueError("path '%s' cannot be absolute" % pathname) |
| 112 | if pathname[-1] == '/': |
| 113 | raise ValueError("path '%s' cannot end with '/'" % pathname) |
| 114 | |
| 115 | paths = pathname.split('/') |
| 116 | while os.curdir in paths: |
| 117 | paths.remove(os.curdir) |
| 118 | if not paths: |
| 119 | return os.curdir |
| 120 | return os.path.join(*paths) |
| 121 | |
| 122 | |
| 123 | def change_root(new_root, pathname): |
| 124 | """Return 'pathname' with 'new_root' prepended. |
| 125 | |
| 126 | If 'pathname' is relative, this is equivalent to |
| 127 | os.path.join(new_root,pathname). Otherwise, it requires making 'pathname' |
| 128 | relative and then joining the two, which is tricky on DOS/Windows. |
| 129 | """ |
| 130 | if os.name == 'posix': |
| 131 | if not os.path.isabs(pathname): |
| 132 | return os.path.join(new_root, pathname) |
| 133 | else: |
| 134 | return os.path.join(new_root, pathname[1:]) |
| 135 | |
| 136 | elif os.name == 'nt': |
| 137 | drive, path = os.path.splitdrive(pathname) |
| 138 | if path[0] == '\\': |
| 139 | path = path[1:] |
| 140 | return os.path.join(new_root, path) |
| 141 | |
| 142 | elif os.name == 'os2': |
| 143 | drive, path = os.path.splitdrive(pathname) |
| 144 | if path[0] == os.sep: |
| 145 | path = path[1:] |
| 146 | return os.path.join(new_root, path) |
| 147 | |
| 148 | else: |
| 149 | raise PackagingPlatformError("nothing known about " |
| 150 | "platform '%s'" % os.name) |
| 151 | |
| 152 | _environ_checked = False |
| 153 | |
| 154 | |
| 155 | def check_environ(): |
| 156 | """Ensure that 'os.environ' has all the environment variables needed. |
| 157 | |
| 158 | We guarantee that users can use in config files, command-line options, |
| 159 | etc. Currently this includes: |
| 160 | HOME - user's home directory (Unix only) |
| 161 | PLAT - description of the current platform, including hardware |
| 162 | and OS (see 'get_platform()') |
| 163 | """ |
| 164 | global _environ_checked |
| 165 | if _environ_checked: |
| 166 | return |
| 167 | |
| 168 | if os.name == 'posix' and 'HOME' not in os.environ: |
| 169 | import pwd |
| 170 | os.environ['HOME'] = pwd.getpwuid(os.getuid())[5] |
| 171 | |
| 172 | if 'PLAT' not in os.environ: |
| 173 | os.environ['PLAT'] = sysconfig.get_platform() |
| 174 | |
| 175 | _environ_checked = True |
| 176 | |
| 177 | |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 178 | # Needed by 'split_quoted()' |
| 179 | _wordchars_re = _squote_re = _dquote_re = None |
| 180 | |
| 181 | |
| 182 | def _init_regex(): |
| 183 | global _wordchars_re, _squote_re, _dquote_re |
| 184 | _wordchars_re = re.compile(r'[^\\\'\"%s ]*' % string.whitespace) |
| 185 | _squote_re = re.compile(r"'(?:[^'\\]|\\.)*'") |
| 186 | _dquote_re = re.compile(r'"(?:[^"\\]|\\.)*"') |
| 187 | |
| 188 | |
Éric Araujo | 95fc53f | 2011-09-01 05:11:29 +0200 | [diff] [blame] | 189 | # TODO replace with shlex.split after testing |
| 190 | |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 191 | def split_quoted(s): |
| 192 | """Split a string up according to Unix shell-like rules for quotes and |
| 193 | backslashes. |
| 194 | |
| 195 | In short: words are delimited by spaces, as long as those |
| 196 | spaces are not escaped by a backslash, or inside a quoted string. |
| 197 | Single and double quotes are equivalent, and the quote characters can |
| 198 | be backslash-escaped. The backslash is stripped from any two-character |
| 199 | escape sequence, leaving only the escaped character. The quote |
| 200 | characters are stripped from any quoted string. Returns a list of |
| 201 | words. |
| 202 | """ |
| 203 | # This is a nice algorithm for splitting up a single string, since it |
| 204 | # doesn't require character-by-character examination. It was a little |
| 205 | # bit of a brain-bender to get it working right, though... |
| 206 | if _wordchars_re is None: |
| 207 | _init_regex() |
| 208 | |
| 209 | s = s.strip() |
| 210 | words = [] |
| 211 | pos = 0 |
| 212 | |
| 213 | while s: |
| 214 | m = _wordchars_re.match(s, pos) |
| 215 | end = m.end() |
| 216 | if end == len(s): |
| 217 | words.append(s[:end]) |
| 218 | break |
| 219 | |
| 220 | if s[end] in string.whitespace: # unescaped, unquoted whitespace: now |
| 221 | words.append(s[:end]) # we definitely have a word delimiter |
| 222 | s = s[end:].lstrip() |
| 223 | pos = 0 |
| 224 | |
| 225 | elif s[end] == '\\': # preserve whatever is being escaped; |
| 226 | # will become part of the current word |
| 227 | s = s[:end] + s[end + 1:] |
| 228 | pos = end + 1 |
| 229 | |
| 230 | else: |
| 231 | if s[end] == "'": # slurp singly-quoted string |
| 232 | m = _squote_re.match(s, end) |
| 233 | elif s[end] == '"': # slurp doubly-quoted string |
| 234 | m = _dquote_re.match(s, end) |
| 235 | else: |
| 236 | raise RuntimeError("this can't happen " |
| 237 | "(bad char '%c')" % s[end]) |
| 238 | |
| 239 | if m is None: |
| 240 | raise ValueError("bad string (mismatched %s quotes?)" % s[end]) |
| 241 | |
| 242 | beg, end = m.span() |
| 243 | s = s[:beg] + s[beg + 1:end - 1] + s[end:] |
| 244 | pos = m.end() - 2 |
| 245 | |
| 246 | if pos >= len(s): |
| 247 | words.append(s) |
| 248 | break |
| 249 | |
| 250 | return words |
| 251 | |
| 252 | |
Éric Araujo | 1c1d9a5 | 2011-06-10 23:26:31 +0200 | [diff] [blame] | 253 | def split_multiline(value): |
| 254 | """Split a multiline string into a list, excluding blank lines.""" |
| 255 | |
| 256 | return [element for element in |
| 257 | (line.strip() for line in value.split('\n')) |
| 258 | if element] |
| 259 | |
| 260 | |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 261 | def execute(func, args, msg=None, verbose=0, dry_run=False): |
| 262 | """Perform some action that affects the outside world. |
| 263 | |
| 264 | Some actions (e.g. writing to the filesystem) are special because |
| 265 | they are disabled by the 'dry_run' flag. This method takes care of all |
| 266 | that bureaucracy for you; all you have to do is supply the |
| 267 | function to call and an argument tuple for it (to embody the |
| 268 | "external action" being performed), and an optional message to |
| 269 | print. |
| 270 | """ |
| 271 | if msg is None: |
| 272 | msg = "%s%r" % (func.__name__, args) |
| 273 | if msg[-2:] == ',)': # correct for singleton tuple |
| 274 | msg = msg[0:-2] + ')' |
| 275 | |
| 276 | logger.info(msg) |
| 277 | if not dry_run: |
| 278 | func(*args) |
| 279 | |
| 280 | |
| 281 | def strtobool(val): |
Éric Araujo | d5d831b | 2011-06-06 01:13:48 +0200 | [diff] [blame] | 282 | """Convert a string representation of truth to a boolean. |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 283 | |
| 284 | True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values |
| 285 | are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if |
| 286 | 'val' is anything else. |
| 287 | """ |
| 288 | val = val.lower() |
| 289 | if val in ('y', 'yes', 't', 'true', 'on', '1'): |
| 290 | return True |
| 291 | elif val in ('n', 'no', 'f', 'false', 'off', '0'): |
| 292 | return False |
| 293 | else: |
| 294 | raise ValueError("invalid truth value %r" % (val,)) |
| 295 | |
| 296 | |
| 297 | def byte_compile(py_files, optimize=0, force=False, prefix=None, |
| 298 | base_dir=None, verbose=0, dry_run=False, direct=None): |
| 299 | """Byte-compile a collection of Python source files to either .pyc |
Éric Araujo | a29e4f6 | 2011-10-08 04:09:15 +0200 | [diff] [blame] | 300 | or .pyo files in a __pycache__ subdirectory. |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 301 | |
| 302 | 'py_files' is a list of files to compile; any files that don't end in |
| 303 | ".py" are silently skipped. 'optimize' must be one of the following: |
| 304 | 0 - don't optimize (generate .pyc) |
| 305 | 1 - normal optimization (like "python -O") |
| 306 | 2 - extra optimization (like "python -OO") |
| 307 | If 'force' is true, all files are recompiled regardless of |
| 308 | timestamps. |
| 309 | |
| 310 | The source filename encoded in each bytecode file defaults to the |
| 311 | filenames listed in 'py_files'; you can modify these with 'prefix' and |
| 312 | 'basedir'. 'prefix' is a string that will be stripped off of each |
| 313 | source filename, and 'base_dir' is a directory name that will be |
| 314 | prepended (after 'prefix' is stripped). You can supply either or both |
| 315 | (or neither) of 'prefix' and 'base_dir', as you wish. |
| 316 | |
| 317 | If 'dry_run' is true, doesn't actually do anything that would |
| 318 | affect the filesystem. |
| 319 | |
| 320 | Byte-compilation is either done directly in this interpreter process |
| 321 | with the standard py_compile module, or indirectly by writing a |
| 322 | temporary script and executing it. Normally, you should let |
| 323 | 'byte_compile()' figure out to use direct compilation or not (see |
| 324 | the source for details). The 'direct' flag is used by the script |
| 325 | generated in indirect mode; unless you know what you're doing, leave |
| 326 | it set to None. |
| 327 | """ |
| 328 | # nothing is done if sys.dont_write_bytecode is True |
| 329 | # FIXME this should not raise an error |
Éric Araujo | 7724a6c | 2011-09-17 03:31:51 +0200 | [diff] [blame] | 330 | if sys.dont_write_bytecode: |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 331 | raise PackagingByteCompileError('byte-compiling is disabled.') |
| 332 | |
| 333 | # First, if the caller didn't force us into direct or indirect mode, |
| 334 | # figure out which mode we should be in. We take a conservative |
| 335 | # approach: choose direct mode *only* if the current interpreter is |
| 336 | # in debug mode and optimize is 0. If we're not in debug mode (-O |
| 337 | # or -OO), we don't know which level of optimization this |
| 338 | # interpreter is running with, so we can't do direct |
| 339 | # byte-compilation and be certain that it's the right thing. Thus, |
| 340 | # always compile indirectly if the current interpreter is in either |
| 341 | # optimize mode, or if either optimization level was requested by |
| 342 | # the caller. |
| 343 | if direct is None: |
| 344 | direct = (__debug__ and optimize == 0) |
| 345 | |
| 346 | # "Indirect" byte-compilation: write a temporary script and then |
| 347 | # run it with the appropriate flags. |
| 348 | if not direct: |
| 349 | from tempfile import mkstemp |
Éric Araujo | 7724a6c | 2011-09-17 03:31:51 +0200 | [diff] [blame] | 350 | # XXX use something better than mkstemp |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 351 | script_fd, script_name = mkstemp(".py") |
Éric Araujo | 7724a6c | 2011-09-17 03:31:51 +0200 | [diff] [blame] | 352 | os.close(script_fd) |
| 353 | script_fd = None |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 354 | logger.info("writing byte-compilation script '%s'", script_name) |
| 355 | if not dry_run: |
| 356 | if script_fd is not None: |
Victor Stinner | 9cf6d13 | 2011-05-19 21:42:47 +0200 | [diff] [blame] | 357 | script = os.fdopen(script_fd, "w", encoding='utf-8') |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 358 | else: |
Victor Stinner | 9cf6d13 | 2011-05-19 21:42:47 +0200 | [diff] [blame] | 359 | script = open(script_name, "w", encoding='utf-8') |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 360 | |
Victor Stinner | 21a9c74 | 2011-05-19 15:51:27 +0200 | [diff] [blame] | 361 | with script: |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 362 | script.write("""\ |
| 363 | from packaging.util import byte_compile |
| 364 | files = [ |
| 365 | """) |
| 366 | |
| 367 | # XXX would be nice to write absolute filenames, just for |
| 368 | # safety's sake (script should be more robust in the face of |
| 369 | # chdir'ing before running it). But this requires abspath'ing |
| 370 | # 'prefix' as well, and that breaks the hack in build_lib's |
| 371 | # 'byte_compile()' method that carefully tacks on a trailing |
| 372 | # slash (os.sep really) to make sure the prefix here is "just |
| 373 | # right". This whole prefix business is rather delicate -- the |
| 374 | # problem is that it's really a directory, but I'm treating it |
| 375 | # as a dumb string, so trailing slashes and so forth matter. |
| 376 | |
| 377 | #py_files = map(os.path.abspath, py_files) |
| 378 | #if prefix: |
| 379 | # prefix = os.path.abspath(prefix) |
| 380 | |
| 381 | script.write(",\n".join(map(repr, py_files)) + "]\n") |
| 382 | script.write(""" |
| 383 | byte_compile(files, optimize=%r, force=%r, |
| 384 | prefix=%r, base_dir=%r, |
| 385 | verbose=%r, dry_run=False, |
| 386 | direct=True) |
| 387 | """ % (optimize, force, prefix, base_dir, verbose)) |
| 388 | |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 389 | cmd = [sys.executable, script_name] |
| 390 | if optimize == 1: |
| 391 | cmd.insert(1, "-O") |
| 392 | elif optimize == 2: |
| 393 | cmd.insert(1, "-OO") |
| 394 | |
Éric Araujo | 088025f | 2011-06-04 18:45:40 +0200 | [diff] [blame] | 395 | env = os.environ.copy() |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 396 | env['PYTHONPATH'] = os.path.pathsep.join(sys.path) |
| 397 | try: |
| 398 | spawn(cmd, env=env) |
| 399 | finally: |
| 400 | execute(os.remove, (script_name,), "removing %s" % script_name, |
| 401 | dry_run=dry_run) |
| 402 | |
| 403 | # "Direct" byte-compilation: use the py_compile module to compile |
| 404 | # right here, right now. Note that the script generated in indirect |
| 405 | # mode simply calls 'byte_compile()' in direct mode, a weird sort of |
| 406 | # cross-process recursion. Hey, it works! |
| 407 | else: |
| 408 | from py_compile import compile |
| 409 | |
| 410 | for file in py_files: |
| 411 | if file[-3:] != ".py": |
| 412 | # This lets us be lazy and not filter filenames in |
| 413 | # the "install_lib" command. |
| 414 | continue |
| 415 | |
| 416 | # Terminology from the py_compile module: |
| 417 | # cfile - byte-compiled file |
| 418 | # dfile - purported source filename (same as 'file' by default) |
Éric Araujo | a29e4f6 | 2011-10-08 04:09:15 +0200 | [diff] [blame] | 419 | if optimize >= 0: |
| 420 | cfile = imp.cache_from_source(file, debug_override=not optimize) |
| 421 | else: |
| 422 | cfile = imp.cache_from_source(file) |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 423 | dfile = file |
| 424 | if prefix: |
| 425 | if file[:len(prefix)] != prefix: |
| 426 | raise ValueError("invalid prefix: filename %r doesn't " |
| 427 | "start with %r" % (file, prefix)) |
| 428 | dfile = dfile[len(prefix):] |
| 429 | if base_dir: |
| 430 | dfile = os.path.join(base_dir, dfile) |
| 431 | |
| 432 | cfile_base = os.path.basename(cfile) |
| 433 | if direct: |
| 434 | if force or newer(file, cfile): |
| 435 | logger.info("byte-compiling %s to %s", file, cfile_base) |
| 436 | if not dry_run: |
| 437 | compile(file, cfile, dfile) |
| 438 | else: |
| 439 | logger.debug("skipping byte-compilation of %s to %s", |
| 440 | file, cfile_base) |
| 441 | |
| 442 | |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 443 | _RE_VERSION = re.compile('(\d+\.\d+(\.\d+)*)') |
| 444 | _MAC_OS_X_LD_VERSION = re.compile('^@\(#\)PROGRAM:ld ' |
| 445 | 'PROJECT:ld64-((\d+)(\.\d+)*)') |
| 446 | |
| 447 | |
| 448 | def _find_ld_version(): |
| 449 | """Find the ld version. The version scheme differs under Mac OS X.""" |
| 450 | if sys.platform == 'darwin': |
| 451 | return _find_exe_version('ld -v', _MAC_OS_X_LD_VERSION) |
| 452 | else: |
| 453 | return _find_exe_version('ld -v') |
| 454 | |
| 455 | |
| 456 | def _find_exe_version(cmd, pattern=_RE_VERSION): |
| 457 | """Find the version of an executable by running `cmd` in the shell. |
| 458 | |
| 459 | `pattern` is a compiled regular expression. If not provided, defaults |
| 460 | to _RE_VERSION. If the command is not found, or the output does not |
| 461 | match the mattern, returns None. |
| 462 | """ |
| 463 | from subprocess import Popen, PIPE |
| 464 | executable = cmd.split()[0] |
| 465 | if find_executable(executable) is None: |
| 466 | return None |
| 467 | pipe = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE) |
| 468 | try: |
Victor Stinner | 9904b22 | 2011-05-21 02:20:36 +0200 | [diff] [blame] | 469 | stdout, stderr = pipe.communicate() |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 470 | finally: |
| 471 | pipe.stdout.close() |
| 472 | pipe.stderr.close() |
| 473 | # some commands like ld under MacOS X, will give the |
| 474 | # output in the stderr, rather than stdout. |
| 475 | if stdout != '': |
| 476 | out_string = stdout |
| 477 | else: |
| 478 | out_string = stderr |
| 479 | |
| 480 | result = pattern.search(out_string) |
| 481 | if result is None: |
| 482 | return None |
| 483 | return result.group(1) |
| 484 | |
| 485 | |
| 486 | def get_compiler_versions(): |
| 487 | """Return a tuple providing the versions of gcc, ld and dllwrap |
| 488 | |
| 489 | For each command, if a command is not found, None is returned. |
| 490 | Otherwise a string with the version is returned. |
| 491 | """ |
| 492 | gcc = _find_exe_version('gcc -dumpversion') |
| 493 | ld = _find_ld_version() |
| 494 | dllwrap = _find_exe_version('dllwrap --version') |
| 495 | return gcc, ld, dllwrap |
| 496 | |
| 497 | |
| 498 | def newer_group(sources, target, missing='error'): |
| 499 | """Return true if 'target' is out-of-date with respect to any file |
| 500 | listed in 'sources'. |
| 501 | |
| 502 | In other words, if 'target' exists and is newer |
| 503 | than every file in 'sources', return false; otherwise return true. |
| 504 | 'missing' controls what we do when a source file is missing; the |
| 505 | default ("error") is to blow up with an OSError from inside 'stat()'; |
| 506 | if it is "ignore", we silently drop any missing source files; if it is |
| 507 | "newer", any missing source files make us assume that 'target' is |
| 508 | out-of-date (this is handy in "dry-run" mode: it'll make you pretend to |
| 509 | carry out commands that wouldn't work because inputs are missing, but |
| 510 | that doesn't matter because you're not actually going to run the |
| 511 | commands). |
| 512 | """ |
| 513 | # If the target doesn't even exist, then it's definitely out-of-date. |
| 514 | if not os.path.exists(target): |
| 515 | return True |
| 516 | |
| 517 | # Otherwise we have to find out the hard way: if *any* source file |
| 518 | # is more recent than 'target', then 'target' is out-of-date and |
| 519 | # we can immediately return true. If we fall through to the end |
| 520 | # of the loop, then 'target' is up-to-date and we return false. |
| 521 | target_mtime = os.stat(target).st_mtime |
| 522 | |
| 523 | for source in sources: |
| 524 | if not os.path.exists(source): |
| 525 | if missing == 'error': # blow up when we stat() the file |
| 526 | pass |
| 527 | elif missing == 'ignore': # missing source dropped from |
| 528 | continue # target's dependency list |
| 529 | elif missing == 'newer': # missing source means target is |
| 530 | return True # out-of-date |
| 531 | |
| 532 | if os.stat(source).st_mtime > target_mtime: |
| 533 | return True |
| 534 | |
| 535 | return False |
| 536 | |
| 537 | |
| 538 | def write_file(filename, contents): |
| 539 | """Create *filename* and write *contents* to it. |
| 540 | |
| 541 | *contents* is a sequence of strings without line terminators. |
Éric Araujo | 95fc53f | 2011-09-01 05:11:29 +0200 | [diff] [blame] | 542 | |
| 543 | This functions is not intended to replace the usual with open + write |
| 544 | idiom in all cases, only with Command.execute, which runs depending on |
| 545 | the dry_run argument and also logs its arguments). |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 546 | """ |
| 547 | with open(filename, "w") as f: |
| 548 | for line in contents: |
| 549 | f.write(line + "\n") |
| 550 | |
| 551 | |
| 552 | def _is_package(path): |
Éric Araujo | 1c1d9a5 | 2011-06-10 23:26:31 +0200 | [diff] [blame] | 553 | return os.path.isdir(path) and os.path.isfile( |
| 554 | os.path.join(path, '__init__.py')) |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 555 | |
| 556 | |
| 557 | # Code taken from the pip project |
| 558 | def _is_archive_file(name): |
| 559 | archives = ('.zip', '.tar.gz', '.tar.bz2', '.tgz', '.tar') |
| 560 | ext = splitext(name)[1].lower() |
Éric Araujo | 1c1d9a5 | 2011-06-10 23:26:31 +0200 | [diff] [blame] | 561 | return ext in archives |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 562 | |
| 563 | |
| 564 | def _under(path, root): |
Éric Araujo | 95fc53f | 2011-09-01 05:11:29 +0200 | [diff] [blame] | 565 | # XXX use os.path |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 566 | path = path.split(os.sep) |
| 567 | root = root.split(os.sep) |
| 568 | if len(root) > len(path): |
| 569 | return False |
| 570 | for pos, part in enumerate(root): |
| 571 | if path[pos] != part: |
| 572 | return False |
| 573 | return True |
| 574 | |
| 575 | |
| 576 | def _package_name(root_path, path): |
| 577 | # Return a dotted package name, given a subpath |
| 578 | if not _under(path, root_path): |
| 579 | raise ValueError('"%s" is not a subpath of "%s"' % (path, root_path)) |
| 580 | return path[len(root_path) + 1:].replace(os.sep, '.') |
| 581 | |
| 582 | |
| 583 | def find_packages(paths=(os.curdir,), exclude=()): |
| 584 | """Return a list all Python packages found recursively within |
| 585 | directories 'paths' |
| 586 | |
| 587 | 'paths' should be supplied as a sequence of "cross-platform" |
| 588 | (i.e. URL-style) path; it will be converted to the appropriate local |
| 589 | path syntax. |
| 590 | |
| 591 | 'exclude' is a sequence of package names to exclude; '*' can be used as |
| 592 | a wildcard in the names, such that 'foo.*' will exclude all subpackages |
| 593 | of 'foo' (but not 'foo' itself). |
| 594 | """ |
| 595 | packages = [] |
| 596 | discarded = [] |
| 597 | |
| 598 | def _discarded(path): |
| 599 | for discard in discarded: |
| 600 | if _under(path, discard): |
| 601 | return True |
| 602 | return False |
| 603 | |
| 604 | for path in paths: |
| 605 | path = convert_path(path) |
| 606 | for root, dirs, files in os.walk(path): |
| 607 | for dir_ in dirs: |
| 608 | fullpath = os.path.join(root, dir_) |
| 609 | if _discarded(fullpath): |
| 610 | continue |
| 611 | # we work only with Python packages |
| 612 | if not _is_package(fullpath): |
| 613 | discarded.append(fullpath) |
| 614 | continue |
| 615 | # see if it's excluded |
| 616 | excluded = False |
| 617 | package_name = _package_name(path, fullpath) |
| 618 | for pattern in exclude: |
| 619 | if fnmatchcase(package_name, pattern): |
| 620 | excluded = True |
| 621 | break |
| 622 | if excluded: |
| 623 | continue |
| 624 | |
| 625 | # adding it to the list |
| 626 | packages.append(package_name) |
| 627 | return packages |
| 628 | |
| 629 | |
| 630 | def resolve_name(name): |
| 631 | """Resolve a name like ``module.object`` to an object and return it. |
| 632 | |
| 633 | Raise ImportError if the module or name is not found. |
| 634 | """ |
| 635 | parts = name.split('.') |
| 636 | cursor = len(parts) |
| 637 | module_name = parts[:cursor] |
| 638 | |
| 639 | while cursor > 0: |
| 640 | try: |
| 641 | ret = __import__('.'.join(module_name)) |
| 642 | break |
| 643 | except ImportError: |
| 644 | if cursor == 0: |
| 645 | raise |
| 646 | cursor -= 1 |
| 647 | module_name = parts[:cursor] |
| 648 | ret = '' |
| 649 | |
| 650 | for part in parts[1:]: |
| 651 | try: |
| 652 | ret = getattr(ret, part) |
| 653 | except AttributeError as exc: |
| 654 | raise ImportError(exc) |
| 655 | |
| 656 | return ret |
| 657 | |
| 658 | |
| 659 | def splitext(path): |
| 660 | """Like os.path.splitext, but take off .tar too""" |
| 661 | base, ext = posixpath.splitext(path) |
| 662 | if base.lower().endswith('.tar'): |
| 663 | ext = base[-4:] + ext |
| 664 | base = base[:-4] |
| 665 | return base, ext |
| 666 | |
| 667 | |
Ned Deily | fceb412 | 2011-06-28 20:04:24 -0700 | [diff] [blame] | 668 | if sys.platform == 'darwin': |
| 669 | _cfg_target = None |
| 670 | _cfg_target_split = None |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 671 | |
Éric Araujo | 95fc53f | 2011-09-01 05:11:29 +0200 | [diff] [blame] | 672 | |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 673 | def spawn(cmd, search_path=True, verbose=0, dry_run=False, env=None): |
| 674 | """Run another program specified as a command list 'cmd' in a new process. |
| 675 | |
| 676 | 'cmd' is just the argument list for the new process, ie. |
| 677 | cmd[0] is the program to run and cmd[1:] are the rest of its arguments. |
| 678 | There is no way to run a program with a name different from that of its |
| 679 | executable. |
| 680 | |
| 681 | If 'search_path' is true (the default), the system's executable |
| 682 | search path will be used to find the program; otherwise, cmd[0] |
| 683 | must be the exact path to the executable. If 'dry_run' is true, |
| 684 | the command will not actually be run. |
| 685 | |
| 686 | If 'env' is given, it's a environment dictionary used for the execution |
| 687 | environment. |
| 688 | |
| 689 | Raise PackagingExecError if running the program fails in any way; just |
| 690 | return on success. |
| 691 | """ |
Éric Araujo | 6280606 | 2011-06-11 09:46:07 +0200 | [diff] [blame] | 692 | logger.debug('spawn: running %r', cmd) |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 693 | if dry_run: |
Éric Araujo | 29f6297 | 2011-08-04 17:17:07 +0200 | [diff] [blame] | 694 | logger.debug('dry run, no process actually spawned') |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 695 | return |
Ned Deily | fceb412 | 2011-06-28 20:04:24 -0700 | [diff] [blame] | 696 | if sys.platform == 'darwin': |
| 697 | global _cfg_target, _cfg_target_split |
| 698 | if _cfg_target is None: |
| 699 | _cfg_target = sysconfig.get_config_var( |
| 700 | 'MACOSX_DEPLOYMENT_TARGET') or '' |
| 701 | if _cfg_target: |
| 702 | _cfg_target_split = [int(x) for x in _cfg_target.split('.')] |
| 703 | if _cfg_target: |
| 704 | # ensure that the deployment target of build process is not less |
| 705 | # than that used when the interpreter was built. This ensures |
| 706 | # extension modules are built with correct compatibility values |
| 707 | env = env or os.environ |
| 708 | cur_target = env.get('MACOSX_DEPLOYMENT_TARGET', _cfg_target) |
| 709 | if _cfg_target_split > [int(x) for x in cur_target.split('.')]: |
| 710 | my_msg = ('$MACOSX_DEPLOYMENT_TARGET mismatch: ' |
| 711 | 'now "%s" but "%s" during configure' |
| 712 | % (cur_target, _cfg_target)) |
| 713 | raise PackagingPlatformError(my_msg) |
| 714 | env = dict(env, MACOSX_DEPLOYMENT_TARGET=cur_target) |
| 715 | |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 716 | exit_status = subprocess.call(cmd, env=env) |
| 717 | if exit_status != 0: |
Éric Araujo | 6280606 | 2011-06-11 09:46:07 +0200 | [diff] [blame] | 718 | msg = "command %r failed with exit status %d" |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 719 | raise PackagingExecError(msg % (cmd, exit_status)) |
| 720 | |
| 721 | |
| 722 | def find_executable(executable, path=None): |
| 723 | """Try to find 'executable' in the directories listed in 'path'. |
| 724 | |
| 725 | *path* is a string listing directories separated by 'os.pathsep' and |
| 726 | defaults to os.environ['PATH']. Returns the complete filename or None |
| 727 | if not found. |
| 728 | """ |
| 729 | if path is None: |
| 730 | path = os.environ['PATH'] |
| 731 | paths = path.split(os.pathsep) |
| 732 | base, ext = os.path.splitext(executable) |
| 733 | |
| 734 | if (sys.platform == 'win32' or os.name == 'os2') and (ext != '.exe'): |
| 735 | executable = executable + '.exe' |
| 736 | |
| 737 | if not os.path.isfile(executable): |
| 738 | for p in paths: |
| 739 | f = os.path.join(p, executable) |
| 740 | if os.path.isfile(f): |
| 741 | # the file exists, we have a shot at spawn working |
| 742 | return f |
| 743 | return None |
| 744 | else: |
| 745 | return executable |
| 746 | |
| 747 | |
| 748 | DEFAULT_REPOSITORY = 'http://pypi.python.org/pypi' |
| 749 | DEFAULT_REALM = 'pypi' |
| 750 | DEFAULT_PYPIRC = """\ |
| 751 | [distutils] |
| 752 | index-servers = |
| 753 | pypi |
| 754 | |
| 755 | [pypi] |
| 756 | username:%s |
| 757 | password:%s |
| 758 | """ |
| 759 | |
| 760 | |
| 761 | def get_pypirc_path(): |
| 762 | """Return path to pypirc config file.""" |
| 763 | return os.path.join(os.path.expanduser('~'), '.pypirc') |
| 764 | |
| 765 | |
| 766 | def generate_pypirc(username, password): |
| 767 | """Create a default .pypirc file.""" |
| 768 | rc = get_pypirc_path() |
| 769 | with open(rc, 'w') as f: |
| 770 | f.write(DEFAULT_PYPIRC % (username, password)) |
| 771 | try: |
| 772 | os.chmod(rc, 0o600) |
| 773 | except OSError: |
| 774 | # should do something better here |
| 775 | pass |
| 776 | |
| 777 | |
| 778 | def read_pypirc(repository=DEFAULT_REPOSITORY, realm=DEFAULT_REALM): |
| 779 | """Read the .pypirc file.""" |
| 780 | rc = get_pypirc_path() |
| 781 | if os.path.exists(rc): |
| 782 | config = RawConfigParser() |
| 783 | config.read(rc) |
| 784 | sections = config.sections() |
| 785 | if 'distutils' in sections: |
| 786 | # let's get the list of servers |
| 787 | index_servers = config.get('distutils', 'index-servers') |
| 788 | _servers = [server.strip() for server in |
| 789 | index_servers.split('\n') |
| 790 | if server.strip() != ''] |
| 791 | if _servers == []: |
| 792 | # nothing set, let's try to get the default pypi |
| 793 | if 'pypi' in sections: |
| 794 | _servers = ['pypi'] |
| 795 | else: |
| 796 | # the file is not properly defined, returning |
| 797 | # an empty dict |
| 798 | return {} |
| 799 | for server in _servers: |
| 800 | current = {'server': server} |
| 801 | current['username'] = config.get(server, 'username') |
| 802 | |
| 803 | # optional params |
| 804 | for key, default in (('repository', DEFAULT_REPOSITORY), |
| 805 | ('realm', DEFAULT_REALM), |
| 806 | ('password', None)): |
| 807 | if config.has_option(server, key): |
| 808 | current[key] = config.get(server, key) |
| 809 | else: |
| 810 | current[key] = default |
| 811 | if (current['server'] == repository or |
| 812 | current['repository'] == repository): |
| 813 | return current |
| 814 | elif 'server-login' in sections: |
| 815 | # old format |
| 816 | server = 'server-login' |
| 817 | if config.has_option(server, 'repository'): |
| 818 | repository = config.get(server, 'repository') |
| 819 | else: |
| 820 | repository = DEFAULT_REPOSITORY |
| 821 | |
| 822 | return {'username': config.get(server, 'username'), |
| 823 | 'password': config.get(server, 'password'), |
| 824 | 'repository': repository, |
| 825 | 'server': server, |
| 826 | 'realm': DEFAULT_REALM} |
| 827 | |
| 828 | return {} |
| 829 | |
| 830 | |
| 831 | # utility functions for 2to3 support |
| 832 | |
| 833 | def run_2to3(files, doctests_only=False, fixer_names=None, |
| 834 | options=None, explicit=None): |
| 835 | """ Wrapper function around the refactor() class which |
| 836 | performs the conversions on a list of python files. |
| 837 | Invoke 2to3 on a list of Python files. The files should all come |
| 838 | from the build area, as the modification is done in-place.""" |
| 839 | |
| 840 | #if not files: |
| 841 | # return |
| 842 | |
| 843 | # Make this class local, to delay import of 2to3 |
| 844 | from lib2to3.refactor import get_fixers_from_package, RefactoringTool |
| 845 | fixers = [] |
| 846 | fixers = get_fixers_from_package('lib2to3.fixes') |
| 847 | |
| 848 | if fixer_names: |
| 849 | for fixername in fixer_names: |
| 850 | fixers.extend(fixer for fixer in |
| 851 | get_fixers_from_package(fixername)) |
| 852 | r = RefactoringTool(fixers, options=options) |
| 853 | r.refactor(files, write=True, doctests_only=doctests_only) |
| 854 | |
| 855 | |
| 856 | class Mixin2to3: |
| 857 | """ Wrapper class for commands that run 2to3. |
| 858 | To configure 2to3, setup scripts may either change |
| 859 | the class variables, or inherit from this class |
| 860 | to override how 2to3 is invoked. |
| 861 | """ |
| 862 | # provide list of fixers to run. |
| 863 | # defaults to all from lib2to3.fixers |
| 864 | fixer_names = None |
| 865 | |
| 866 | # options dictionary |
| 867 | options = None |
| 868 | |
| 869 | # list of fixers to invoke even though they are marked as explicit |
| 870 | explicit = None |
| 871 | |
| 872 | def run_2to3(self, files, doctests_only=False): |
| 873 | """ Issues a call to util.run_2to3. """ |
| 874 | return run_2to3(files, doctests_only, self.fixer_names, |
| 875 | self.options, self.explicit) |
| 876 | |
| 877 | RICH_GLOB = re.compile(r'\{([^}]*)\}') |
Tarek Ziade | ec9b76d | 2011-05-21 11:48:16 +0200 | [diff] [blame] | 878 | _CHECK_RECURSIVE_GLOB = re.compile(r'[^/\\,{]\*\*|\*\*[^/\\,}]') |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 879 | _CHECK_MISMATCH_SET = re.compile(r'^[^{]*\}|\{[^}]*$') |
| 880 | |
| 881 | |
| 882 | def iglob(path_glob): |
| 883 | """Extended globbing function that supports ** and {opt1,opt2,opt3}.""" |
| 884 | if _CHECK_RECURSIVE_GLOB.search(path_glob): |
| 885 | msg = """invalid glob %r: recursive glob "**" must be used alone""" |
| 886 | raise ValueError(msg % path_glob) |
| 887 | if _CHECK_MISMATCH_SET.search(path_glob): |
| 888 | msg = """invalid glob %r: mismatching set marker '{' or '}'""" |
| 889 | raise ValueError(msg % path_glob) |
| 890 | return _iglob(path_glob) |
| 891 | |
| 892 | |
| 893 | def _iglob(path_glob): |
| 894 | rich_path_glob = RICH_GLOB.split(path_glob, 1) |
| 895 | if len(rich_path_glob) > 1: |
| 896 | assert len(rich_path_glob) == 3, rich_path_glob |
| 897 | prefix, set, suffix = rich_path_glob |
| 898 | for item in set.split(','): |
| 899 | for path in _iglob(''.join((prefix, item, suffix))): |
| 900 | yield path |
| 901 | else: |
| 902 | if '**' not in path_glob: |
| 903 | for item in std_iglob(path_glob): |
| 904 | yield item |
| 905 | else: |
| 906 | prefix, radical = path_glob.split('**', 1) |
| 907 | if prefix == '': |
| 908 | prefix = '.' |
| 909 | if radical == '': |
| 910 | radical = '*' |
| 911 | else: |
Tarek Ziade | ec9b76d | 2011-05-21 11:48:16 +0200 | [diff] [blame] | 912 | # we support both |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 913 | radical = radical.lstrip('/') |
Tarek Ziade | ec9b76d | 2011-05-21 11:48:16 +0200 | [diff] [blame] | 914 | radical = radical.lstrip('\\') |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 915 | for path, dir, files in os.walk(prefix): |
| 916 | path = os.path.normpath(path) |
| 917 | for file in _iglob(os.path.join(path, radical)): |
| 918 | yield file |
| 919 | |
| 920 | |
| 921 | def cfg_to_args(path='setup.cfg'): |
| 922 | """Compatibility helper to use setup.cfg in setup.py. |
| 923 | |
| 924 | This functions uses an existing setup.cfg to generate a dictionnary of |
| 925 | keywords that can be used by distutils.core.setup(**kwargs). It is used |
| 926 | by generate_setup_py. |
| 927 | |
| 928 | *file* is the path to the setup.cfg file. If it doesn't exist, |
| 929 | PackagingFileError is raised. |
| 930 | """ |
| 931 | # We need to declare the following constants here so that it's easier to |
| 932 | # generate the setup.py afterwards, using inspect.getsource. |
| 933 | |
| 934 | # XXX ** == needs testing |
| 935 | D1_D2_SETUP_ARGS = {"name": ("metadata",), |
| 936 | "version": ("metadata",), |
| 937 | "author": ("metadata",), |
| 938 | "author_email": ("metadata",), |
| 939 | "maintainer": ("metadata",), |
| 940 | "maintainer_email": ("metadata",), |
| 941 | "url": ("metadata", "home_page"), |
| 942 | "description": ("metadata", "summary"), |
| 943 | "long_description": ("metadata", "description"), |
| 944 | "download-url": ("metadata",), |
| 945 | "classifiers": ("metadata", "classifier"), |
| 946 | "platforms": ("metadata", "platform"), # ** |
| 947 | "license": ("metadata",), |
| 948 | "requires": ("metadata", "requires_dist"), |
| 949 | "provides": ("metadata", "provides_dist"), # ** |
| 950 | "obsoletes": ("metadata", "obsoletes_dist"), # ** |
Éric Araujo | 3605030 | 2011-06-10 23:52:26 +0200 | [diff] [blame] | 951 | "package_dir": ("files", 'packages_root'), |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 952 | "packages": ("files",), |
| 953 | "scripts": ("files",), |
| 954 | "py_modules": ("files", "modules"), # ** |
| 955 | } |
| 956 | |
| 957 | MULTI_FIELDS = ("classifiers", |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 958 | "platforms", |
Éric Araujo | 3605030 | 2011-06-10 23:52:26 +0200 | [diff] [blame] | 959 | "requires", |
| 960 | "provides", |
| 961 | "obsoletes", |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 962 | "packages", |
Éric Araujo | 3605030 | 2011-06-10 23:52:26 +0200 | [diff] [blame] | 963 | "scripts", |
| 964 | "py_modules") |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 965 | |
| 966 | def has_get_option(config, section, option): |
| 967 | if config.has_option(section, option): |
| 968 | return config.get(section, option) |
| 969 | elif config.has_option(section, option.replace('_', '-')): |
| 970 | return config.get(section, option.replace('_', '-')) |
| 971 | else: |
| 972 | return False |
| 973 | |
| 974 | # The real code starts here |
| 975 | config = RawConfigParser() |
Éric Araujo | 3605030 | 2011-06-10 23:52:26 +0200 | [diff] [blame] | 976 | if not os.path.exists(path): |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 977 | raise PackagingFileError("file '%s' does not exist" % |
Éric Araujo | 3605030 | 2011-06-10 23:52:26 +0200 | [diff] [blame] | 978 | os.path.abspath(path)) |
Éric Araujo | 8d233f2 | 2011-06-12 23:02:57 +0200 | [diff] [blame] | 979 | config.read(path, encoding='utf-8') |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 980 | |
| 981 | kwargs = {} |
| 982 | for arg in D1_D2_SETUP_ARGS: |
| 983 | if len(D1_D2_SETUP_ARGS[arg]) == 2: |
| 984 | # The distutils field name is different than packaging's |
| 985 | section, option = D1_D2_SETUP_ARGS[arg] |
| 986 | |
| 987 | else: |
| 988 | # The distutils field name is the same thant packaging's |
| 989 | section = D1_D2_SETUP_ARGS[arg][0] |
| 990 | option = arg |
| 991 | |
| 992 | in_cfg_value = has_get_option(config, section, option) |
| 993 | if not in_cfg_value: |
| 994 | # There is no such option in the setup.cfg |
Éric Araujo | 3605030 | 2011-06-10 23:52:26 +0200 | [diff] [blame] | 995 | if arg == 'long_description': |
| 996 | filenames = has_get_option(config, section, 'description-file') |
| 997 | if filenames: |
| 998 | filenames = split_multiline(filenames) |
| 999 | in_cfg_value = [] |
| 1000 | for filename in filenames: |
| 1001 | with open(filename) as fp: |
| 1002 | in_cfg_value.append(fp.read()) |
| 1003 | in_cfg_value = '\n\n'.join(in_cfg_value) |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 1004 | else: |
| 1005 | continue |
| 1006 | |
Éric Araujo | 3605030 | 2011-06-10 23:52:26 +0200 | [diff] [blame] | 1007 | if arg == 'package_dir' and in_cfg_value: |
| 1008 | in_cfg_value = {'': in_cfg_value} |
| 1009 | |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 1010 | if arg in MULTI_FIELDS: |
| 1011 | # support multiline options |
Éric Araujo | 3605030 | 2011-06-10 23:52:26 +0200 | [diff] [blame] | 1012 | in_cfg_value = split_multiline(in_cfg_value) |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 1013 | |
| 1014 | kwargs[arg] = in_cfg_value |
| 1015 | |
| 1016 | return kwargs |
| 1017 | |
| 1018 | |
| 1019 | _SETUP_TMPL = """\ |
| 1020 | # This script was automatically generated by packaging |
| 1021 | import os |
| 1022 | from distutils.core import setup |
| 1023 | from ConfigParser import RawConfigParser |
| 1024 | |
| 1025 | %(func)s |
| 1026 | |
| 1027 | setup(**cfg_to_args()) |
| 1028 | """ |
| 1029 | |
| 1030 | |
| 1031 | def generate_setup_py(): |
| 1032 | """Generate a distutils compatible setup.py using an existing setup.cfg. |
| 1033 | |
| 1034 | Raises a PackagingFileError when a setup.py already exists. |
| 1035 | """ |
| 1036 | if os.path.exists("setup.py"): |
Tarek Ziade | 721ccd0 | 2011-06-02 12:00:44 +0200 | [diff] [blame] | 1037 | raise PackagingFileError("a setup.py file already exists") |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 1038 | |
Victor Stinner | 9cf6d13 | 2011-05-19 21:42:47 +0200 | [diff] [blame] | 1039 | with open("setup.py", "w", encoding='utf-8') as fp: |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 1040 | fp.write(_SETUP_TMPL % {'func': getsource(cfg_to_args)}) |
| 1041 | |
| 1042 | |
| 1043 | # Taken from the pip project |
| 1044 | # https://github.com/pypa/pip/blob/master/pip/util.py |
| 1045 | def ask(message, options): |
| 1046 | """Prompt the user with *message*; *options* contains allowed responses.""" |
| 1047 | while True: |
| 1048 | response = input(message) |
| 1049 | response = response.strip().lower() |
| 1050 | if response not in options: |
Éric Araujo | 3cab2f1 | 2011-06-08 04:10:57 +0200 | [diff] [blame] | 1051 | print('invalid response:', repr(response)) |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 1052 | print('choose one of', ', '.join(repr(o) for o in options)) |
| 1053 | else: |
| 1054 | return response |
| 1055 | |
| 1056 | |
| 1057 | def _parse_record_file(record_file): |
| 1058 | distinfo, extra_metadata, installed = ({}, [], []) |
| 1059 | with open(record_file, 'r') as rfile: |
| 1060 | for path in rfile: |
| 1061 | path = path.strip() |
| 1062 | if path.endswith('egg-info') and os.path.isfile(path): |
| 1063 | distinfo_dir = path.replace('egg-info', 'dist-info') |
| 1064 | metadata = path |
| 1065 | egginfo = path |
| 1066 | elif path.endswith('egg-info') and os.path.isdir(path): |
| 1067 | distinfo_dir = path.replace('egg-info', 'dist-info') |
| 1068 | egginfo = path |
| 1069 | for metadata_file in os.listdir(path): |
| 1070 | metadata_fpath = os.path.join(path, metadata_file) |
| 1071 | if metadata_file == 'PKG-INFO': |
| 1072 | metadata = metadata_fpath |
| 1073 | else: |
| 1074 | extra_metadata.append(metadata_fpath) |
| 1075 | elif 'egg-info' in path and os.path.isfile(path): |
| 1076 | # skip extra metadata files |
| 1077 | continue |
| 1078 | else: |
| 1079 | installed.append(path) |
| 1080 | |
| 1081 | distinfo['egginfo'] = egginfo |
| 1082 | distinfo['metadata'] = metadata |
| 1083 | distinfo['distinfo_dir'] = distinfo_dir |
| 1084 | distinfo['installer_path'] = os.path.join(distinfo_dir, 'INSTALLER') |
| 1085 | distinfo['metadata_path'] = os.path.join(distinfo_dir, 'METADATA') |
| 1086 | distinfo['record_path'] = os.path.join(distinfo_dir, 'RECORD') |
| 1087 | distinfo['requested_path'] = os.path.join(distinfo_dir, 'REQUESTED') |
| 1088 | installed.extend([distinfo['installer_path'], distinfo['metadata_path']]) |
| 1089 | distinfo['installed'] = installed |
| 1090 | distinfo['extra_metadata'] = extra_metadata |
| 1091 | return distinfo |
| 1092 | |
| 1093 | |
| 1094 | def _write_record_file(record_path, installed_files): |
| 1095 | with open(record_path, 'w', encoding='utf-8') as f: |
| 1096 | writer = csv.writer(f, delimiter=',', lineterminator=os.linesep, |
| 1097 | quotechar='"') |
| 1098 | |
| 1099 | for fpath in installed_files: |
| 1100 | if fpath.endswith('.pyc') or fpath.endswith('.pyo'): |
| 1101 | # do not put size and md5 hash, as in PEP-376 |
| 1102 | writer.writerow((fpath, '', '')) |
| 1103 | else: |
| 1104 | hash = hashlib.md5() |
| 1105 | with open(fpath, 'rb') as fp: |
| 1106 | hash.update(fp.read()) |
| 1107 | md5sum = hash.hexdigest() |
| 1108 | size = os.path.getsize(fpath) |
| 1109 | writer.writerow((fpath, md5sum, size)) |
| 1110 | |
| 1111 | # add the RECORD file itself |
| 1112 | writer.writerow((record_path, '', '')) |
| 1113 | return record_path |
| 1114 | |
| 1115 | |
| 1116 | def egginfo_to_distinfo(record_file, installer=_DEFAULT_INSTALLER, |
| 1117 | requested=False, remove_egginfo=False): |
| 1118 | """Create files and directories required for PEP 376 |
| 1119 | |
| 1120 | :param record_file: path to RECORD file as produced by setup.py --record |
| 1121 | :param installer: installer name |
| 1122 | :param requested: True if not installed as a dependency |
| 1123 | :param remove_egginfo: delete egginfo dir? |
| 1124 | """ |
| 1125 | distinfo = _parse_record_file(record_file) |
| 1126 | distinfo_dir = distinfo['distinfo_dir'] |
| 1127 | if os.path.isdir(distinfo_dir) and not os.path.islink(distinfo_dir): |
| 1128 | shutil.rmtree(distinfo_dir) |
| 1129 | elif os.path.exists(distinfo_dir): |
| 1130 | os.unlink(distinfo_dir) |
| 1131 | |
| 1132 | os.makedirs(distinfo_dir) |
| 1133 | |
| 1134 | # copy setuptools extra metadata files |
| 1135 | if distinfo['extra_metadata']: |
| 1136 | for path in distinfo['extra_metadata']: |
| 1137 | shutil.copy2(path, distinfo_dir) |
| 1138 | new_path = path.replace('egg-info', 'dist-info') |
| 1139 | distinfo['installed'].append(new_path) |
| 1140 | |
| 1141 | metadata_path = distinfo['metadata_path'] |
| 1142 | logger.info('creating %s', metadata_path) |
| 1143 | shutil.copy2(distinfo['metadata'], metadata_path) |
| 1144 | |
| 1145 | installer_path = distinfo['installer_path'] |
| 1146 | logger.info('creating %s', installer_path) |
| 1147 | with open(installer_path, 'w') as f: |
| 1148 | f.write(installer) |
| 1149 | |
| 1150 | if requested: |
| 1151 | requested_path = distinfo['requested_path'] |
| 1152 | logger.info('creating %s', requested_path) |
Victor Stinner | 4c9706b | 2011-05-19 15:52:59 +0200 | [diff] [blame] | 1153 | open(requested_path, 'wb').close() |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 1154 | distinfo['installed'].append(requested_path) |
| 1155 | |
| 1156 | record_path = distinfo['record_path'] |
| 1157 | logger.info('creating %s', record_path) |
| 1158 | _write_record_file(record_path, distinfo['installed']) |
| 1159 | |
| 1160 | if remove_egginfo: |
| 1161 | egginfo = distinfo['egginfo'] |
| 1162 | logger.info('removing %s', egginfo) |
| 1163 | if os.path.isfile(egginfo): |
| 1164 | os.remove(egginfo) |
| 1165 | else: |
| 1166 | shutil.rmtree(egginfo) |
| 1167 | |
| 1168 | |
| 1169 | def _has_egg_info(srcdir): |
| 1170 | if os.path.isdir(srcdir): |
| 1171 | for item in os.listdir(srcdir): |
| 1172 | full_path = os.path.join(srcdir, item) |
| 1173 | if item.endswith('.egg-info') and os.path.isdir(full_path): |
Tarek Ziade | b1b6e13 | 2011-05-30 12:07:49 +0200 | [diff] [blame] | 1174 | logger.debug("Found egg-info directory.") |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 1175 | return True |
Tarek Ziade | b1b6e13 | 2011-05-30 12:07:49 +0200 | [diff] [blame] | 1176 | logger.debug("No egg-info directory found.") |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 1177 | return False |
| 1178 | |
| 1179 | |
| 1180 | def _has_setuptools_text(setup_py): |
| 1181 | return _has_text(setup_py, 'setuptools') |
| 1182 | |
| 1183 | |
| 1184 | def _has_distutils_text(setup_py): |
| 1185 | return _has_text(setup_py, 'distutils') |
| 1186 | |
| 1187 | |
| 1188 | def _has_text(setup_py, installer): |
| 1189 | installer_pattern = re.compile('import {0}|from {0}'.format(installer)) |
| 1190 | with open(setup_py, 'r', encoding='utf-8') as setup: |
| 1191 | for line in setup: |
| 1192 | if re.search(installer_pattern, line): |
Tarek Ziade | b1b6e13 | 2011-05-30 12:07:49 +0200 | [diff] [blame] | 1193 | logger.debug("Found %s text in setup.py.", installer) |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 1194 | return True |
Tarek Ziade | b1b6e13 | 2011-05-30 12:07:49 +0200 | [diff] [blame] | 1195 | logger.debug("No %s text found in setup.py.", installer) |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 1196 | return False |
| 1197 | |
| 1198 | |
| 1199 | def _has_required_metadata(setup_cfg): |
| 1200 | config = RawConfigParser() |
| 1201 | config.read([setup_cfg], encoding='utf8') |
| 1202 | return (config.has_section('metadata') and |
| 1203 | 'name' in config.options('metadata') and |
| 1204 | 'version' in config.options('metadata')) |
| 1205 | |
| 1206 | |
| 1207 | def _has_pkg_info(srcdir): |
| 1208 | pkg_info = os.path.join(srcdir, 'PKG-INFO') |
| 1209 | has_pkg_info = os.path.isfile(pkg_info) |
| 1210 | if has_pkg_info: |
Tarek Ziade | b1b6e13 | 2011-05-30 12:07:49 +0200 | [diff] [blame] | 1211 | logger.debug("PKG-INFO file found.") |
| 1212 | else: |
| 1213 | logger.debug("No PKG-INFO file found.") |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 1214 | return has_pkg_info |
| 1215 | |
| 1216 | |
| 1217 | def _has_setup_py(srcdir): |
| 1218 | setup_py = os.path.join(srcdir, 'setup.py') |
| 1219 | if os.path.isfile(setup_py): |
Tarek Ziade | b1b6e13 | 2011-05-30 12:07:49 +0200 | [diff] [blame] | 1220 | logger.debug('setup.py file found.') |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 1221 | return True |
| 1222 | return False |
| 1223 | |
| 1224 | |
| 1225 | def _has_setup_cfg(srcdir): |
| 1226 | setup_cfg = os.path.join(srcdir, 'setup.cfg') |
| 1227 | if os.path.isfile(setup_cfg): |
Tarek Ziade | b1b6e13 | 2011-05-30 12:07:49 +0200 | [diff] [blame] | 1228 | logger.debug('setup.cfg file found.') |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 1229 | return True |
Tarek Ziade | b1b6e13 | 2011-05-30 12:07:49 +0200 | [diff] [blame] | 1230 | logger.debug("No setup.cfg file found.") |
Tarek Ziade | 1231a4e | 2011-05-19 13:07:25 +0200 | [diff] [blame] | 1231 | return False |
| 1232 | |
| 1233 | |
| 1234 | def is_setuptools(path): |
| 1235 | """Check if the project is based on setuptools. |
| 1236 | |
| 1237 | :param path: path to source directory containing a setup.py script. |
| 1238 | |
| 1239 | Return True if the project requires setuptools to install, else False. |
| 1240 | """ |
| 1241 | srcdir = os.path.abspath(path) |
| 1242 | setup_py = os.path.join(srcdir, 'setup.py') |
| 1243 | |
| 1244 | return _has_setup_py(srcdir) and (_has_egg_info(srcdir) or |
| 1245 | _has_setuptools_text(setup_py)) |
| 1246 | |
| 1247 | |
| 1248 | def is_distutils(path): |
| 1249 | """Check if the project is based on distutils. |
| 1250 | |
| 1251 | :param path: path to source directory containing a setup.py script. |
| 1252 | |
| 1253 | Return True if the project requires distutils to install, else False. |
| 1254 | """ |
| 1255 | srcdir = os.path.abspath(path) |
| 1256 | setup_py = os.path.join(srcdir, 'setup.py') |
| 1257 | |
| 1258 | return _has_setup_py(srcdir) and (_has_pkg_info(srcdir) or |
| 1259 | _has_distutils_text(setup_py)) |
| 1260 | |
| 1261 | |
| 1262 | def is_packaging(path): |
| 1263 | """Check if the project is based on packaging |
| 1264 | |
| 1265 | :param path: path to source directory containing a setup.cfg file. |
| 1266 | |
| 1267 | Return True if the project has a valid setup.cfg, else False. |
| 1268 | """ |
| 1269 | srcdir = os.path.abspath(path) |
| 1270 | setup_cfg = os.path.join(srcdir, 'setup.cfg') |
| 1271 | |
| 1272 | return _has_setup_cfg(srcdir) and _has_required_metadata(setup_cfg) |
| 1273 | |
| 1274 | |
| 1275 | def get_install_method(path): |
| 1276 | """Check if the project is based on packaging, setuptools, or distutils |
| 1277 | |
| 1278 | :param path: path to source directory containing a setup.cfg file, |
| 1279 | or setup.py. |
| 1280 | |
| 1281 | Returns a string representing the best install method to use. |
| 1282 | """ |
| 1283 | if is_packaging(path): |
| 1284 | return "packaging" |
| 1285 | elif is_setuptools(path): |
| 1286 | return "setuptools" |
| 1287 | elif is_distutils(path): |
| 1288 | return "distutils" |
| 1289 | else: |
| 1290 | raise InstallationException('Cannot detect install method') |
| 1291 | |
| 1292 | |
| 1293 | # XXX to be replaced by shutil.copytree |
| 1294 | def copy_tree(src, dst, preserve_mode=True, preserve_times=True, |
| 1295 | preserve_symlinks=False, update=False, verbose=True, |
| 1296 | dry_run=False): |
| 1297 | from distutils.file_util import copy_file |
| 1298 | |
| 1299 | if not dry_run and not os.path.isdir(src): |
| 1300 | raise PackagingFileError( |
| 1301 | "cannot copy tree '%s': not a directory" % src) |
| 1302 | try: |
| 1303 | names = os.listdir(src) |
| 1304 | except os.error as e: |
| 1305 | errstr = e[1] |
| 1306 | if dry_run: |
| 1307 | names = [] |
| 1308 | else: |
| 1309 | raise PackagingFileError( |
| 1310 | "error listing files in '%s': %s" % (src, errstr)) |
| 1311 | |
| 1312 | if not dry_run: |
| 1313 | _mkpath(dst, verbose=verbose) |
| 1314 | |
| 1315 | outputs = [] |
| 1316 | |
| 1317 | for n in names: |
| 1318 | src_name = os.path.join(src, n) |
| 1319 | dst_name = os.path.join(dst, n) |
| 1320 | |
| 1321 | if preserve_symlinks and os.path.islink(src_name): |
| 1322 | link_dest = os.readlink(src_name) |
| 1323 | if verbose >= 1: |
| 1324 | logger.info("linking %s -> %s", dst_name, link_dest) |
| 1325 | if not dry_run: |
| 1326 | os.symlink(link_dest, dst_name) |
| 1327 | outputs.append(dst_name) |
| 1328 | |
| 1329 | elif os.path.isdir(src_name): |
| 1330 | outputs.extend( |
| 1331 | copy_tree(src_name, dst_name, preserve_mode, |
| 1332 | preserve_times, preserve_symlinks, update, |
| 1333 | verbose=verbose, dry_run=dry_run)) |
| 1334 | else: |
| 1335 | copy_file(src_name, dst_name, preserve_mode, |
| 1336 | preserve_times, update, verbose=verbose, |
| 1337 | dry_run=dry_run) |
| 1338 | outputs.append(dst_name) |
| 1339 | |
| 1340 | return outputs |
| 1341 | |
| 1342 | # cache for by mkpath() -- in addition to cheapening redundant calls, |
| 1343 | # eliminates redundant "creating /foo/bar/baz" messages in dry-run mode |
| 1344 | _path_created = set() |
| 1345 | |
| 1346 | |
| 1347 | # I don't use os.makedirs because a) it's new to Python 1.5.2, and |
| 1348 | # b) it blows up if the directory already exists (I want to silently |
| 1349 | # succeed in that case). |
| 1350 | def _mkpath(name, mode=0o777, verbose=True, dry_run=False): |
| 1351 | # Detect a common bug -- name is None |
| 1352 | if not isinstance(name, str): |
| 1353 | raise PackagingInternalError( |
| 1354 | "mkpath: 'name' must be a string (got %r)" % (name,)) |
| 1355 | |
| 1356 | # XXX what's the better way to handle verbosity? print as we create |
| 1357 | # each directory in the path (the current behaviour), or only announce |
| 1358 | # the creation of the whole path? (quite easy to do the latter since |
| 1359 | # we're not using a recursive algorithm) |
| 1360 | |
| 1361 | name = os.path.normpath(name) |
| 1362 | created_dirs = [] |
| 1363 | if os.path.isdir(name) or name == '': |
| 1364 | return created_dirs |
| 1365 | if os.path.abspath(name) in _path_created: |
| 1366 | return created_dirs |
| 1367 | |
| 1368 | head, tail = os.path.split(name) |
| 1369 | tails = [tail] # stack of lone dirs to create |
| 1370 | |
| 1371 | while head and tail and not os.path.isdir(head): |
| 1372 | head, tail = os.path.split(head) |
| 1373 | tails.insert(0, tail) # push next higher dir onto stack |
| 1374 | |
| 1375 | # now 'head' contains the deepest directory that already exists |
| 1376 | # (that is, the child of 'head' in 'name' is the highest directory |
| 1377 | # that does *not* exist) |
| 1378 | for d in tails: |
| 1379 | head = os.path.join(head, d) |
| 1380 | abs_head = os.path.abspath(head) |
| 1381 | |
| 1382 | if abs_head in _path_created: |
| 1383 | continue |
| 1384 | |
| 1385 | if verbose >= 1: |
| 1386 | logger.info("creating %s", head) |
| 1387 | |
| 1388 | if not dry_run: |
| 1389 | try: |
| 1390 | os.mkdir(head, mode) |
| 1391 | except OSError as exc: |
| 1392 | if not (exc.errno == errno.EEXIST and os.path.isdir(head)): |
| 1393 | raise PackagingFileError( |
| 1394 | "could not create '%s': %s" % (head, exc.args[-1])) |
| 1395 | created_dirs.append(head) |
| 1396 | |
| 1397 | _path_created.add(abs_head) |
| 1398 | return created_dirs |
Éric Araujo | ce5fe83 | 2011-07-08 16:27:12 +0200 | [diff] [blame] | 1399 | |
| 1400 | |
| 1401 | def encode_multipart(fields, files, boundary=None): |
| 1402 | """Prepare a multipart HTTP request. |
| 1403 | |
| 1404 | *fields* is a sequence of (name: str, value: str) elements for regular |
| 1405 | form fields, *files* is a sequence of (name: str, filename: str, value: |
| 1406 | bytes) elements for data to be uploaded as files. |
| 1407 | |
| 1408 | Returns (content_type: bytes, body: bytes) ready for http.client.HTTP. |
| 1409 | """ |
| 1410 | # Taken from |
| 1411 | # http://code.activestate.com/recipes/146306-http-client-to-post-using-multipartform-data/ |
| 1412 | |
| 1413 | if boundary is None: |
| 1414 | boundary = b'--------------GHSKFJDLGDS7543FJKLFHRE75642756743254' |
| 1415 | elif not isinstance(boundary, bytes): |
| 1416 | raise TypeError('boundary must be bytes, not %r' % type(boundary)) |
| 1417 | |
| 1418 | l = [] |
| 1419 | for key, values in fields: |
| 1420 | # handle multiple entries for the same name |
| 1421 | if not isinstance(values, (tuple, list)): |
Éric Araujo | 95fc53f | 2011-09-01 05:11:29 +0200 | [diff] [blame] | 1422 | values = [values] |
Éric Araujo | ce5fe83 | 2011-07-08 16:27:12 +0200 | [diff] [blame] | 1423 | |
| 1424 | for value in values: |
| 1425 | l.extend(( |
| 1426 | b'--' + boundary, |
| 1427 | ('Content-Disposition: form-data; name="%s"' % |
| 1428 | key).encode('utf-8'), |
| 1429 | b'', |
| 1430 | value.encode('utf-8'))) |
| 1431 | |
| 1432 | for key, filename, value in files: |
| 1433 | l.extend(( |
| 1434 | b'--' + boundary, |
| 1435 | ('Content-Disposition: form-data; name="%s"; filename="%s"' % |
| 1436 | (key, filename)).encode('utf-8'), |
| 1437 | b'', |
| 1438 | value)) |
| 1439 | |
| 1440 | l.append(b'--' + boundary + b'--') |
| 1441 | l.append(b'') |
| 1442 | |
| 1443 | body = b'\r\n'.join(l) |
| 1444 | content_type = b'multipart/form-data; boundary=' + boundary |
| 1445 | return content_type, body |