PageRenderTime 53ms CodeModel.GetById 12ms RepoModel.GetById 1ms app.codeStats 0ms

/venv/lib/python2.7/site-packages/pip/_vendor/distlib/util.py

https://gitlab.com/ismailam/openexport
Python | 1536 lines | 1470 code | 34 blank | 32 comment | 39 complexity | 3d24cedd5cdc80f0cc05e0822bd74991 MD5 | raw file
  1. #
  2. # Copyright (C) 2012-2014 The Python Software Foundation.
  3. # See LICENSE.txt and CONTRIBUTORS.txt.
  4. #
  5. import codecs
  6. from collections import deque
  7. import contextlib
  8. import csv
  9. from glob import iglob as std_iglob
  10. import io
  11. import json
  12. import logging
  13. import os
  14. import py_compile
  15. import re
  16. import shutil
  17. import socket
  18. import ssl
  19. import subprocess
  20. import sys
  21. import tarfile
  22. import tempfile
  23. try:
  24. import threading
  25. except ImportError:
  26. import dummy_threading as threading
  27. import time
  28. from . import DistlibException
  29. from .compat import (string_types, text_type, shutil, raw_input, StringIO,
  30. cache_from_source, urlopen, httplib, xmlrpclib, splittype,
  31. HTTPHandler, HTTPSHandler as BaseHTTPSHandler,
  32. BaseConfigurator, valid_ident, Container, configparser,
  33. URLError, match_hostname, CertificateError, ZipFile)
  34. logger = logging.getLogger(__name__)
  35. #
  36. # Requirement parsing code for name + optional constraints + optional extras
  37. #
  38. # e.g. 'foo >= 1.2, < 2.0 [bar, baz]'
  39. #
  40. # The regex can seem a bit hairy, so we build it up out of smaller pieces
  41. # which are manageable.
  42. #
  43. COMMA = r'\s*,\s*'
  44. COMMA_RE = re.compile(COMMA)
  45. IDENT = r'(\w|[.-])+'
  46. EXTRA_IDENT = r'(\*|:(\*|\w+):|' + IDENT + ')'
  47. VERSPEC = IDENT + r'\*?'
  48. RELOP = '([<>=!~]=)|[<>]'
  49. #
  50. # The first relop is optional - if absent, will be taken as '~='
  51. #
  52. BARE_CONSTRAINTS = ('(' + RELOP + r')?\s*(' + VERSPEC + ')(' + COMMA + '(' +
  53. RELOP + r')\s*(' + VERSPEC + '))*')
  54. DIRECT_REF = '(from\s+(?P<diref>.*))'
  55. #
  56. # Either the bare constraints or the bare constraints in parentheses
  57. #
  58. CONSTRAINTS = (r'\(\s*(?P<c1>' + BARE_CONSTRAINTS + '|' + DIRECT_REF +
  59. r')\s*\)|(?P<c2>' + BARE_CONSTRAINTS + '\s*)')
  60. EXTRA_LIST = EXTRA_IDENT + '(' + COMMA + EXTRA_IDENT + ')*'
  61. EXTRAS = r'\[\s*(?P<ex>' + EXTRA_LIST + r')?\s*\]'
  62. REQUIREMENT = ('(?P<dn>' + IDENT + r')\s*(' + EXTRAS + r'\s*)?(\s*' +
  63. CONSTRAINTS + ')?$')
  64. REQUIREMENT_RE = re.compile(REQUIREMENT)
  65. #
  66. # Used to scan through the constraints
  67. #
  68. RELOP_IDENT = '(?P<op>' + RELOP + r')\s*(?P<vn>' + VERSPEC + ')'
  69. RELOP_IDENT_RE = re.compile(RELOP_IDENT)
  70. def parse_requirement(s):
  71. def get_constraint(m):
  72. d = m.groupdict()
  73. return d['op'], d['vn']
  74. result = None
  75. m = REQUIREMENT_RE.match(s)
  76. if m:
  77. d = m.groupdict()
  78. name = d['dn']
  79. cons = d['c1'] or d['c2']
  80. if not d['diref']:
  81. url = None
  82. else:
  83. # direct reference
  84. cons = None
  85. url = d['diref'].strip()
  86. if not cons:
  87. cons = None
  88. constr = ''
  89. rs = d['dn']
  90. else:
  91. if cons[0] not in '<>!=':
  92. cons = '~=' + cons
  93. iterator = RELOP_IDENT_RE.finditer(cons)
  94. cons = [get_constraint(m) for m in iterator]
  95. rs = '%s (%s)' % (name, ', '.join(['%s %s' % con for con in cons]))
  96. if not d['ex']:
  97. extras = None
  98. else:
  99. extras = COMMA_RE.split(d['ex'])
  100. result = Container(name=name, constraints=cons, extras=extras,
  101. requirement=rs, source=s, url=url)
  102. return result
  103. def get_resources_dests(resources_root, rules):
  104. """Find destinations for resources files"""
  105. def get_rel_path(base, path):
  106. # normalizes and returns a lstripped-/-separated path
  107. base = base.replace(os.path.sep, '/')
  108. path = path.replace(os.path.sep, '/')
  109. assert path.startswith(base)
  110. return path[len(base):].lstrip('/')
  111. destinations = {}
  112. for base, suffix, dest in rules:
  113. prefix = os.path.join(resources_root, base)
  114. for abs_base in iglob(prefix):
  115. abs_glob = os.path.join(abs_base, suffix)
  116. for abs_path in iglob(abs_glob):
  117. resource_file = get_rel_path(resources_root, abs_path)
  118. if dest is None: # remove the entry if it was here
  119. destinations.pop(resource_file, None)
  120. else:
  121. rel_path = get_rel_path(abs_base, abs_path)
  122. rel_dest = dest.replace(os.path.sep, '/').rstrip('/')
  123. destinations[resource_file] = rel_dest + '/' + rel_path
  124. return destinations
  125. def in_venv():
  126. if hasattr(sys, 'real_prefix'):
  127. # virtualenv venvs
  128. result = True
  129. else:
  130. # PEP 405 venvs
  131. result = sys.prefix != getattr(sys, 'base_prefix', sys.prefix)
  132. return result
  133. def get_executable():
  134. # The __PYVENV_LAUNCHER__ dance is apparently no longer needed, as
  135. # changes to the stub launcher mean that sys.executable always points
  136. # to the stub on OS X
  137. # if sys.platform == 'darwin' and ('__PYVENV_LAUNCHER__'
  138. # in os.environ):
  139. # result = os.environ['__PYVENV_LAUNCHER__']
  140. # else:
  141. # result = sys.executable
  142. # return result
  143. return sys.executable
  144. def proceed(prompt, allowed_chars, error_prompt=None, default=None):
  145. p = prompt
  146. while True:
  147. s = raw_input(p)
  148. p = prompt
  149. if not s and default:
  150. s = default
  151. if s:
  152. c = s[0].lower()
  153. if c in allowed_chars:
  154. break
  155. if error_prompt:
  156. p = '%c: %s\n%s' % (c, error_prompt, prompt)
  157. return c
  158. def extract_by_key(d, keys):
  159. if isinstance(keys, string_types):
  160. keys = keys.split()
  161. result = {}
  162. for key in keys:
  163. if key in d:
  164. result[key] = d[key]
  165. return result
  166. def read_exports(stream):
  167. if sys.version_info[0] >= 3:
  168. # needs to be a text stream
  169. stream = codecs.getreader('utf-8')(stream)
  170. # Try to load as JSON, falling back on legacy format
  171. data = stream.read()
  172. stream = StringIO(data)
  173. try:
  174. data = json.load(stream)
  175. result = data['extensions']['python.exports']['exports']
  176. for group, entries in result.items():
  177. for k, v in entries.items():
  178. s = '%s = %s' % (k, v)
  179. entry = get_export_entry(s)
  180. assert entry is not None
  181. entries[k] = entry
  182. return result
  183. except Exception:
  184. stream.seek(0, 0)
  185. cp = configparser.ConfigParser()
  186. if hasattr(cp, 'read_file'):
  187. cp.read_file(stream)
  188. else:
  189. cp.readfp(stream)
  190. result = {}
  191. for key in cp.sections():
  192. result[key] = entries = {}
  193. for name, value in cp.items(key):
  194. s = '%s = %s' % (name, value)
  195. entry = get_export_entry(s)
  196. assert entry is not None
  197. #entry.dist = self
  198. entries[name] = entry
  199. return result
  200. def write_exports(exports, stream):
  201. if sys.version_info[0] >= 3:
  202. # needs to be a text stream
  203. stream = codecs.getwriter('utf-8')(stream)
  204. cp = configparser.ConfigParser()
  205. for k, v in exports.items():
  206. # TODO check k, v for valid values
  207. cp.add_section(k)
  208. for entry in v.values():
  209. if entry.suffix is None:
  210. s = entry.prefix
  211. else:
  212. s = '%s:%s' % (entry.prefix, entry.suffix)
  213. if entry.flags:
  214. s = '%s [%s]' % (s, ', '.join(entry.flags))
  215. cp.set(k, entry.name, s)
  216. cp.write(stream)
  217. @contextlib.contextmanager
  218. def tempdir():
  219. td = tempfile.mkdtemp()
  220. try:
  221. yield td
  222. finally:
  223. shutil.rmtree(td)
  224. @contextlib.contextmanager
  225. def chdir(d):
  226. cwd = os.getcwd()
  227. try:
  228. os.chdir(d)
  229. yield
  230. finally:
  231. os.chdir(cwd)
  232. @contextlib.contextmanager
  233. def socket_timeout(seconds=15):
  234. cto = socket.getdefaulttimeout()
  235. try:
  236. socket.setdefaulttimeout(seconds)
  237. yield
  238. finally:
  239. socket.setdefaulttimeout(cto)
  240. class cached_property(object):
  241. def __init__(self, func):
  242. self.func = func
  243. #for attr in ('__name__', '__module__', '__doc__'):
  244. # setattr(self, attr, getattr(func, attr, None))
  245. def __get__(self, obj, cls=None):
  246. if obj is None:
  247. return self
  248. value = self.func(obj)
  249. object.__setattr__(obj, self.func.__name__, value)
  250. #obj.__dict__[self.func.__name__] = value = self.func(obj)
  251. return value
  252. def convert_path(pathname):
  253. """Return 'pathname' as a name that will work on the native filesystem.
  254. The path is split on '/' and put back together again using the current
  255. directory separator. Needed because filenames in the setup script are
  256. always supplied in Unix style, and have to be converted to the local
  257. convention before we can actually use them in the filesystem. Raises
  258. ValueError on non-Unix-ish systems if 'pathname' either starts or
  259. ends with a slash.
  260. """
  261. if os.sep == '/':
  262. return pathname
  263. if not pathname:
  264. return pathname
  265. if pathname[0] == '/':
  266. raise ValueError("path '%s' cannot be absolute" % pathname)
  267. if pathname[-1] == '/':
  268. raise ValueError("path '%s' cannot end with '/'" % pathname)
  269. paths = pathname.split('/')
  270. while os.curdir in paths:
  271. paths.remove(os.curdir)
  272. if not paths:
  273. return os.curdir
  274. return os.path.join(*paths)
  275. class FileOperator(object):
  276. def __init__(self, dry_run=False):
  277. self.dry_run = dry_run
  278. self.ensured = set()
  279. self._init_record()
  280. def _init_record(self):
  281. self.record = False
  282. self.files_written = set()
  283. self.dirs_created = set()
  284. def record_as_written(self, path):
  285. if self.record:
  286. self.files_written.add(path)
  287. def newer(self, source, target):
  288. """Tell if the target is newer than the source.
  289. Returns true if 'source' exists and is more recently modified than
  290. 'target', or if 'source' exists and 'target' doesn't.
  291. Returns false if both exist and 'target' is the same age or younger
  292. than 'source'. Raise PackagingFileError if 'source' does not exist.
  293. Note that this test is not very accurate: files created in the same
  294. second will have the same "age".
  295. """
  296. if not os.path.exists(source):
  297. raise DistlibException("file '%r' does not exist" %
  298. os.path.abspath(source))
  299. if not os.path.exists(target):
  300. return True
  301. return os.stat(source).st_mtime > os.stat(target).st_mtime
  302. def copy_file(self, infile, outfile, check=True):
  303. """Copy a file respecting dry-run and force flags.
  304. """
  305. self.ensure_dir(os.path.dirname(outfile))
  306. logger.info('Copying %s to %s', infile, outfile)
  307. if not self.dry_run:
  308. msg = None
  309. if check:
  310. if os.path.islink(outfile):
  311. msg = '%s is a symlink' % outfile
  312. elif os.path.exists(outfile) and not os.path.isfile(outfile):
  313. msg = '%s is a non-regular file' % outfile
  314. if msg:
  315. raise ValueError(msg + ' which would be overwritten')
  316. shutil.copyfile(infile, outfile)
  317. self.record_as_written(outfile)
  318. def copy_stream(self, instream, outfile, encoding=None):
  319. assert not os.path.isdir(outfile)
  320. self.ensure_dir(os.path.dirname(outfile))
  321. logger.info('Copying stream %s to %s', instream, outfile)
  322. if not self.dry_run:
  323. if encoding is None:
  324. outstream = open(outfile, 'wb')
  325. else:
  326. outstream = codecs.open(outfile, 'w', encoding=encoding)
  327. try:
  328. shutil.copyfileobj(instream, outstream)
  329. finally:
  330. outstream.close()
  331. self.record_as_written(outfile)
  332. def write_binary_file(self, path, data):
  333. self.ensure_dir(os.path.dirname(path))
  334. if not self.dry_run:
  335. with open(path, 'wb') as f:
  336. f.write(data)
  337. self.record_as_written(path)
  338. def write_text_file(self, path, data, encoding):
  339. self.ensure_dir(os.path.dirname(path))
  340. if not self.dry_run:
  341. with open(path, 'wb') as f:
  342. f.write(data.encode(encoding))
  343. self.record_as_written(path)
  344. def set_mode(self, bits, mask, files):
  345. if os.name == 'posix':
  346. # Set the executable bits (owner, group, and world) on
  347. # all the files specified.
  348. for f in files:
  349. if self.dry_run:
  350. logger.info("changing mode of %s", f)
  351. else:
  352. mode = (os.stat(f).st_mode | bits) & mask
  353. logger.info("changing mode of %s to %o", f, mode)
  354. os.chmod(f, mode)
  355. set_executable_mode = lambda s, f: s.set_mode(0o555, 0o7777, f)
  356. def ensure_dir(self, path):
  357. path = os.path.abspath(path)
  358. if path not in self.ensured and not os.path.exists(path):
  359. self.ensured.add(path)
  360. d, f = os.path.split(path)
  361. self.ensure_dir(d)
  362. logger.info('Creating %s' % path)
  363. if not self.dry_run:
  364. os.mkdir(path)
  365. if self.record:
  366. self.dirs_created.add(path)
  367. def byte_compile(self, path, optimize=False, force=False, prefix=None):
  368. dpath = cache_from_source(path, not optimize)
  369. logger.info('Byte-compiling %s to %s', path, dpath)
  370. if not self.dry_run:
  371. if force or self.newer(path, dpath):
  372. if not prefix:
  373. diagpath = None
  374. else:
  375. assert path.startswith(prefix)
  376. diagpath = path[len(prefix):]
  377. py_compile.compile(path, dpath, diagpath, True) # raise error
  378. self.record_as_written(dpath)
  379. return dpath
  380. def ensure_removed(self, path):
  381. if os.path.exists(path):
  382. if os.path.isdir(path) and not os.path.islink(path):
  383. logger.debug('Removing directory tree at %s', path)
  384. if not self.dry_run:
  385. shutil.rmtree(path)
  386. if self.record:
  387. if path in self.dirs_created:
  388. self.dirs_created.remove(path)
  389. else:
  390. if os.path.islink(path):
  391. s = 'link'
  392. else:
  393. s = 'file'
  394. logger.debug('Removing %s %s', s, path)
  395. if not self.dry_run:
  396. os.remove(path)
  397. if self.record:
  398. if path in self.files_written:
  399. self.files_written.remove(path)
  400. def is_writable(self, path):
  401. result = False
  402. while not result:
  403. if os.path.exists(path):
  404. result = os.access(path, os.W_OK)
  405. break
  406. parent = os.path.dirname(path)
  407. if parent == path:
  408. break
  409. path = parent
  410. return result
  411. def commit(self):
  412. """
  413. Commit recorded changes, turn off recording, return
  414. changes.
  415. """
  416. assert self.record
  417. result = self.files_written, self.dirs_created
  418. self._init_record()
  419. return result
  420. def rollback(self):
  421. if not self.dry_run:
  422. for f in list(self.files_written):
  423. if os.path.exists(f):
  424. os.remove(f)
  425. # dirs should all be empty now, except perhaps for
  426. # __pycache__ subdirs
  427. # reverse so that subdirs appear before their parents
  428. dirs = sorted(self.dirs_created, reverse=True)
  429. for d in dirs:
  430. flist = os.listdir(d)
  431. if flist:
  432. assert flist == ['__pycache__']
  433. sd = os.path.join(d, flist[0])
  434. os.rmdir(sd)
  435. os.rmdir(d) # should fail if non-empty
  436. self._init_record()
  437. def resolve(module_name, dotted_path):
  438. if module_name in sys.modules:
  439. mod = sys.modules[module_name]
  440. else:
  441. mod = __import__(module_name)
  442. if dotted_path is None:
  443. result = mod
  444. else:
  445. parts = dotted_path.split('.')
  446. result = getattr(mod, parts.pop(0))
  447. for p in parts:
  448. result = getattr(result, p)
  449. return result
  450. class ExportEntry(object):
  451. def __init__(self, name, prefix, suffix, flags):
  452. self.name = name
  453. self.prefix = prefix
  454. self.suffix = suffix
  455. self.flags = flags
  456. @cached_property
  457. def value(self):
  458. return resolve(self.prefix, self.suffix)
  459. def __repr__(self):
  460. return '<ExportEntry %s = %s:%s %s>' % (self.name, self.prefix,
  461. self.suffix, self.flags)
  462. def __eq__(self, other):
  463. if not isinstance(other, ExportEntry):
  464. result = False
  465. else:
  466. result = (self.name == other.name and
  467. self.prefix == other.prefix and
  468. self.suffix == other.suffix and
  469. self.flags == other.flags)
  470. return result
  471. __hash__ = object.__hash__
  472. ENTRY_RE = re.compile(r'''(?P<name>(\w|[-.])+)
  473. \s*=\s*(?P<callable>(\w+)([:\.]\w+)*)
  474. \s*(\[\s*(?P<flags>\w+(=\w+)?(,\s*\w+(=\w+)?)*)\s*\])?
  475. ''', re.VERBOSE)
  476. def get_export_entry(specification):
  477. m = ENTRY_RE.search(specification)
  478. if not m:
  479. result = None
  480. if '[' in specification or ']' in specification:
  481. raise DistlibException('Invalid specification '
  482. '%r' % specification)
  483. else:
  484. d = m.groupdict()
  485. name = d['name']
  486. path = d['callable']
  487. colons = path.count(':')
  488. if colons == 0:
  489. prefix, suffix = path, None
  490. else:
  491. if colons != 1:
  492. raise DistlibException('Invalid specification '
  493. '%r' % specification)
  494. prefix, suffix = path.split(':')
  495. flags = d['flags']
  496. if flags is None:
  497. if '[' in specification or ']' in specification:
  498. raise DistlibException('Invalid specification '
  499. '%r' % specification)
  500. flags = []
  501. else:
  502. flags = [f.strip() for f in flags.split(',')]
  503. result = ExportEntry(name, prefix, suffix, flags)
  504. return result
  505. def get_cache_base(suffix=None):
  506. """
  507. Return the default base location for distlib caches. If the directory does
  508. not exist, it is created. Use the suffix provided for the base directory,
  509. and default to '.distlib' if it isn't provided.
  510. On Windows, if LOCALAPPDATA is defined in the environment, then it is
  511. assumed to be a directory, and will be the parent directory of the result.
  512. On POSIX, and on Windows if LOCALAPPDATA is not defined, the user's home
  513. directory - using os.expanduser('~') - will be the parent directory of
  514. the result.
  515. The result is just the directory '.distlib' in the parent directory as
  516. determined above, or with the name specified with ``suffix``.
  517. """
  518. if suffix is None:
  519. suffix = '.distlib'
  520. if os.name == 'nt' and 'LOCALAPPDATA' in os.environ:
  521. result = os.path.expandvars('$localappdata')
  522. else:
  523. # Assume posix, or old Windows
  524. result = os.path.expanduser('~')
  525. # we use 'isdir' instead of 'exists', because we want to
  526. # fail if there's a file with that name
  527. if os.path.isdir(result):
  528. usable = os.access(result, os.W_OK)
  529. if not usable:
  530. logger.warning('Directory exists but is not writable: %s', result)
  531. else:
  532. try:
  533. os.makedirs(result)
  534. usable = True
  535. except OSError:
  536. logger.warning('Unable to create %s', result, exc_info=True)
  537. usable = False
  538. if not usable:
  539. result = tempfile.mkdtemp()
  540. logger.warning('Default location unusable, using %s', result)
  541. return os.path.join(result, suffix)
  542. def path_to_cache_dir(path):
  543. """
  544. Convert an absolute path to a directory name for use in a cache.
  545. The algorithm used is:
  546. #. On Windows, any ``':'`` in the drive is replaced with ``'---'``.
  547. #. Any occurrence of ``os.sep`` is replaced with ``'--'``.
  548. #. ``'.cache'`` is appended.
  549. """
  550. d, p = os.path.splitdrive(os.path.abspath(path))
  551. if d:
  552. d = d.replace(':', '---')
  553. p = p.replace(os.sep, '--')
  554. return d + p + '.cache'
  555. def ensure_slash(s):
  556. if not s.endswith('/'):
  557. return s + '/'
  558. return s
  559. def parse_credentials(netloc):
  560. username = password = None
  561. if '@' in netloc:
  562. prefix, netloc = netloc.split('@', 1)
  563. if ':' not in prefix:
  564. username = prefix
  565. else:
  566. username, password = prefix.split(':', 1)
  567. return username, password, netloc
  568. def get_process_umask():
  569. result = os.umask(0o22)
  570. os.umask(result)
  571. return result
  572. def is_string_sequence(seq):
  573. result = True
  574. i = None
  575. for i, s in enumerate(seq):
  576. if not isinstance(s, string_types):
  577. result = False
  578. break
  579. assert i is not None
  580. return result
  581. PROJECT_NAME_AND_VERSION = re.compile('([a-z0-9_]+([.-][a-z_][a-z0-9_]*)*)-'
  582. '([a-z0-9_.+-]+)', re.I)
  583. PYTHON_VERSION = re.compile(r'-py(\d\.?\d?)')
  584. def split_filename(filename, project_name=None):
  585. """
  586. Extract name, version, python version from a filename (no extension)
  587. Return name, version, pyver or None
  588. """
  589. result = None
  590. pyver = None
  591. m = PYTHON_VERSION.search(filename)
  592. if m:
  593. pyver = m.group(1)
  594. filename = filename[:m.start()]
  595. if project_name and len(filename) > len(project_name) + 1:
  596. m = re.match(re.escape(project_name) + r'\b', filename)
  597. if m:
  598. n = m.end()
  599. result = filename[:n], filename[n + 1:], pyver
  600. if result is None:
  601. m = PROJECT_NAME_AND_VERSION.match(filename)
  602. if m:
  603. result = m.group(1), m.group(3), pyver
  604. return result
  605. # Allow spaces in name because of legacy dists like "Twisted Core"
  606. NAME_VERSION_RE = re.compile(r'(?P<name>[\w .-]+)\s*'
  607. r'\(\s*(?P<ver>[^\s)]+)\)$')
  608. def parse_name_and_version(p):
  609. """
  610. A utility method used to get name and version from a string.
  611. From e.g. a Provides-Dist value.
  612. :param p: A value in a form 'foo (1.0)'
  613. :return: The name and version as a tuple.
  614. """
  615. m = NAME_VERSION_RE.match(p)
  616. if not m:
  617. raise DistlibException('Ill-formed name/version string: \'%s\'' % p)
  618. d = m.groupdict()
  619. return d['name'].strip().lower(), d['ver']
  620. def get_extras(requested, available):
  621. result = set()
  622. requested = set(requested or [])
  623. available = set(available or [])
  624. if '*' in requested:
  625. requested.remove('*')
  626. result |= available
  627. for r in requested:
  628. if r == '-':
  629. result.add(r)
  630. elif r.startswith('-'):
  631. unwanted = r[1:]
  632. if unwanted not in available:
  633. logger.warning('undeclared extra: %s' % unwanted)
  634. if unwanted in result:
  635. result.remove(unwanted)
  636. else:
  637. if r not in available:
  638. logger.warning('undeclared extra: %s' % r)
  639. result.add(r)
  640. return result
  641. #
  642. # Extended metadata functionality
  643. #
  644. def _get_external_data(url):
  645. result = {}
  646. try:
  647. # urlopen might fail if it runs into redirections,
  648. # because of Python issue #13696. Fixed in locators
  649. # using a custom redirect handler.
  650. resp = urlopen(url)
  651. headers = resp.info()
  652. if headers.get('Content-Type') != 'application/json':
  653. logger.debug('Unexpected response for JSON request')
  654. else:
  655. reader = codecs.getreader('utf-8')(resp)
  656. #data = reader.read().decode('utf-8')
  657. #result = json.loads(data)
  658. result = json.load(reader)
  659. except Exception as e:
  660. logger.exception('Failed to get external data for %s: %s', url, e)
  661. return result
  662. def get_project_data(name):
  663. url = ('https://www.red-dove.com/pypi/projects/'
  664. '%s/%s/project.json' % (name[0].upper(), name))
  665. result = _get_external_data(url)
  666. return result
  667. def get_package_data(name, version):
  668. url = ('https://www.red-dove.com/pypi/projects/'
  669. '%s/%s/package-%s.json' % (name[0].upper(), name, version))
  670. return _get_external_data(url)
  671. class Cache(object):
  672. """
  673. A class implementing a cache for resources that need to live in the file system
  674. e.g. shared libraries. This class was moved from resources to here because it
  675. could be used by other modules, e.g. the wheel module.
  676. """
  677. def __init__(self, base):
  678. """
  679. Initialise an instance.
  680. :param base: The base directory where the cache should be located.
  681. """
  682. # we use 'isdir' instead of 'exists', because we want to
  683. # fail if there's a file with that name
  684. if not os.path.isdir(base):
  685. os.makedirs(base)
  686. if (os.stat(base).st_mode & 0o77) != 0:
  687. logger.warning('Directory \'%s\' is not private', base)
  688. self.base = os.path.abspath(os.path.normpath(base))
  689. def prefix_to_dir(self, prefix):
  690. """
  691. Converts a resource prefix to a directory name in the cache.
  692. """
  693. return path_to_cache_dir(prefix)
  694. def clear(self):
  695. """
  696. Clear the cache.
  697. """
  698. not_removed = []
  699. for fn in os.listdir(self.base):
  700. fn = os.path.join(self.base, fn)
  701. try:
  702. if os.path.islink(fn) or os.path.isfile(fn):
  703. os.remove(fn)
  704. elif os.path.isdir(fn):
  705. shutil.rmtree(fn)
  706. except Exception:
  707. not_removed.append(fn)
  708. return not_removed
  709. class EventMixin(object):
  710. """
  711. A very simple publish/subscribe system.
  712. """
  713. def __init__(self):
  714. self._subscribers = {}
  715. def add(self, event, subscriber, append=True):
  716. """
  717. Add a subscriber for an event.
  718. :param event: The name of an event.
  719. :param subscriber: The subscriber to be added (and called when the
  720. event is published).
  721. :param append: Whether to append or prepend the subscriber to an
  722. existing subscriber list for the event.
  723. """
  724. subs = self._subscribers
  725. if event not in subs:
  726. subs[event] = deque([subscriber])
  727. else:
  728. sq = subs[event]
  729. if append:
  730. sq.append(subscriber)
  731. else:
  732. sq.appendleft(subscriber)
  733. def remove(self, event, subscriber):
  734. """
  735. Remove a subscriber for an event.
  736. :param event: The name of an event.
  737. :param subscriber: The subscriber to be removed.
  738. """
  739. subs = self._subscribers
  740. if event not in subs:
  741. raise ValueError('No subscribers: %r' % event)
  742. subs[event].remove(subscriber)
  743. def get_subscribers(self, event):
  744. """
  745. Return an iterator for the subscribers for an event.
  746. :param event: The event to return subscribers for.
  747. """
  748. return iter(self._subscribers.get(event, ()))
  749. def publish(self, event, *args, **kwargs):
  750. """
  751. Publish a event and return a list of values returned by its
  752. subscribers.
  753. :param event: The event to publish.
  754. :param args: The positional arguments to pass to the event's
  755. subscribers.
  756. :param kwargs: The keyword arguments to pass to the event's
  757. subscribers.
  758. """
  759. result = []
  760. for subscriber in self.get_subscribers(event):
  761. try:
  762. value = subscriber(event, *args, **kwargs)
  763. except Exception:
  764. logger.exception('Exception during event publication')
  765. value = None
  766. result.append(value)
  767. logger.debug('publish %s: args = %s, kwargs = %s, result = %s',
  768. event, args, kwargs, result)
  769. return result
  770. #
  771. # Simple sequencing
  772. #
  773. class Sequencer(object):
  774. def __init__(self):
  775. self._preds = {}
  776. self._succs = {}
  777. self._nodes = set() # nodes with no preds/succs
  778. def add_node(self, node):
  779. self._nodes.add(node)
  780. def remove_node(self, node, edges=False):
  781. if node in self._nodes:
  782. self._nodes.remove(node)
  783. if edges:
  784. for p in set(self._preds.get(node, ())):
  785. self.remove(p, node)
  786. for s in set(self._succs.get(node, ())):
  787. self.remove(node, s)
  788. # Remove empties
  789. for k, v in list(self._preds.items()):
  790. if not v:
  791. del self._preds[k]
  792. for k, v in list(self._succs.items()):
  793. if not v:
  794. del self._succs[k]
  795. def add(self, pred, succ):
  796. assert pred != succ
  797. self._preds.setdefault(succ, set()).add(pred)
  798. self._succs.setdefault(pred, set()).add(succ)
  799. def remove(self, pred, succ):
  800. assert pred != succ
  801. try:
  802. preds = self._preds[succ]
  803. succs = self._succs[pred]
  804. except KeyError:
  805. raise ValueError('%r not a successor of anything' % succ)
  806. try:
  807. preds.remove(pred)
  808. succs.remove(succ)
  809. except KeyError:
  810. raise ValueError('%r not a successor of %r' % (succ, pred))
  811. def is_step(self, step):
  812. return (step in self._preds or step in self._succs or
  813. step in self._nodes)
  814. def get_steps(self, final):
  815. if not self.is_step(final):
  816. raise ValueError('Unknown: %r' % final)
  817. result = []
  818. todo = []
  819. seen = set()
  820. todo.append(final)
  821. while todo:
  822. step = todo.pop(0)
  823. if step in seen:
  824. # if a step was already seen,
  825. # move it to the end (so it will appear earlier
  826. # when reversed on return) ... but not for the
  827. # final step, as that would be confusing for
  828. # users
  829. if step != final:
  830. result.remove(step)
  831. result.append(step)
  832. else:
  833. seen.add(step)
  834. result.append(step)
  835. preds = self._preds.get(step, ())
  836. todo.extend(preds)
  837. return reversed(result)
  838. @property
  839. def strong_connections(self):
  840. #http://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm
  841. index_counter = [0]
  842. stack = []
  843. lowlinks = {}
  844. index = {}
  845. result = []
  846. graph = self._succs
  847. def strongconnect(node):
  848. # set the depth index for this node to the smallest unused index
  849. index[node] = index_counter[0]
  850. lowlinks[node] = index_counter[0]
  851. index_counter[0] += 1
  852. stack.append(node)
  853. # Consider successors
  854. try:
  855. successors = graph[node]
  856. except Exception:
  857. successors = []
  858. for successor in successors:
  859. if successor not in lowlinks:
  860. # Successor has not yet been visited
  861. strongconnect(successor)
  862. lowlinks[node] = min(lowlinks[node],lowlinks[successor])
  863. elif successor in stack:
  864. # the successor is in the stack and hence in the current
  865. # strongly connected component (SCC)
  866. lowlinks[node] = min(lowlinks[node],index[successor])
  867. # If `node` is a root node, pop the stack and generate an SCC
  868. if lowlinks[node] == index[node]:
  869. connected_component = []
  870. while True:
  871. successor = stack.pop()
  872. connected_component.append(successor)
  873. if successor == node: break
  874. component = tuple(connected_component)
  875. # storing the result
  876. result.append(component)
  877. for node in graph:
  878. if node not in lowlinks:
  879. strongconnect(node)
  880. return result
  881. @property
  882. def dot(self):
  883. result = ['digraph G {']
  884. for succ in self._preds:
  885. preds = self._preds[succ]
  886. for pred in preds:
  887. result.append(' %s -> %s;' % (pred, succ))
  888. for node in self._nodes:
  889. result.append(' %s;' % node)
  890. result.append('}')
  891. return '\n'.join(result)
  892. #
  893. # Unarchiving functionality for zip, tar, tgz, tbz, whl
  894. #
  895. ARCHIVE_EXTENSIONS = ('.tar.gz', '.tar.bz2', '.tar', '.zip',
  896. '.tgz', '.tbz', '.whl')
  897. def unarchive(archive_filename, dest_dir, format=None, check=True):
  898. def check_path(path):
  899. if not isinstance(path, text_type):
  900. path = path.decode('utf-8')
  901. p = os.path.abspath(os.path.join(dest_dir, path))
  902. if not p.startswith(dest_dir) or p[plen] != os.sep:
  903. raise ValueError('path outside destination: %r' % p)
  904. dest_dir = os.path.abspath(dest_dir)
  905. plen = len(dest_dir)
  906. archive = None
  907. if format is None:
  908. if archive_filename.endswith(('.zip', '.whl')):
  909. format = 'zip'
  910. elif archive_filename.endswith(('.tar.gz', '.tgz')):
  911. format = 'tgz'
  912. mode = 'r:gz'
  913. elif archive_filename.endswith(('.tar.bz2', '.tbz')):
  914. format = 'tbz'
  915. mode = 'r:bz2'
  916. elif archive_filename.endswith('.tar'):
  917. format = 'tar'
  918. mode = 'r'
  919. else:
  920. raise ValueError('Unknown format for %r' % archive_filename)
  921. try:
  922. if format == 'zip':
  923. archive = ZipFile(archive_filename, 'r')
  924. if check:
  925. names = archive.namelist()
  926. for name in names:
  927. check_path(name)
  928. else:
  929. archive = tarfile.open(archive_filename, mode)
  930. if check:
  931. names = archive.getnames()
  932. for name in names:
  933. check_path(name)
  934. if format != 'zip' and sys.version_info[0] < 3:
  935. # See Python issue 17153. If the dest path contains Unicode,
  936. # tarfile extraction fails on Python 2.x if a member path name
  937. # contains non-ASCII characters - it leads to an implicit
  938. # bytes -> unicode conversion using ASCII to decode.
  939. for tarinfo in archive.getmembers():
  940. if not isinstance(tarinfo.name, text_type):
  941. tarinfo.name = tarinfo.name.decode('utf-8')
  942. archive.extractall(dest_dir)
  943. finally:
  944. if archive:
  945. archive.close()
  946. def zip_dir(directory):
  947. """zip a directory tree into a BytesIO object"""
  948. result = io.BytesIO()
  949. dlen = len(directory)
  950. with ZipFile(result, "w") as zf:
  951. for root, dirs, files in os.walk(directory):
  952. for name in files:
  953. full = os.path.join(root, name)
  954. rel = root[dlen:]
  955. dest = os.path.join(rel, name)
  956. zf.write(full, dest)
  957. return result
  958. #
  959. # Simple progress bar
  960. #
  961. UNITS = ('', 'K', 'M', 'G','T','P')
  962. class Progress(object):
  963. unknown = 'UNKNOWN'
  964. def __init__(self, minval=0, maxval=100):
  965. assert maxval is None or maxval >= minval
  966. self.min = self.cur = minval
  967. self.max = maxval
  968. self.started = None
  969. self.elapsed = 0
  970. self.done = False
  971. def update(self, curval):
  972. assert self.min <= curval
  973. assert self.max is None or curval <= self.max
  974. self.cur = curval
  975. now = time.time()
  976. if self.started is None:
  977. self.started = now
  978. else:
  979. self.elapsed = now - self.started
  980. def increment(self, incr):
  981. assert incr >= 0
  982. self.update(self.cur + incr)
  983. def start(self):
  984. self.update(self.min)
  985. return self
  986. def stop(self):
  987. if self.max is not None:
  988. self.update(self.max)
  989. self.done = True
  990. @property
  991. def maximum(self):
  992. return self.unknown if self.max is None else self.max
  993. @property
  994. def percentage(self):
  995. if self.done:
  996. result = '100 %'
  997. elif self.max is None:
  998. result = ' ?? %'
  999. else:
  1000. v = 100.0 * (self.cur - self.min) / (self.max - self.min)
  1001. result = '%3d %%' % v
  1002. return result
  1003. def format_duration(self, duration):
  1004. if (duration <= 0) and self.max is None or self.cur == self.min:
  1005. result = '??:??:??'
  1006. #elif duration < 1:
  1007. # result = '--:--:--'
  1008. else:
  1009. result = time.strftime('%H:%M:%S', time.gmtime(duration))
  1010. return result
  1011. @property
  1012. def ETA(self):
  1013. if self.done:
  1014. prefix = 'Done'
  1015. t = self.elapsed
  1016. #import pdb; pdb.set_trace()
  1017. else:
  1018. prefix = 'ETA '
  1019. if self.max is None:
  1020. t = -1
  1021. elif self.elapsed == 0 or (self.cur == self.min):
  1022. t = 0
  1023. else:
  1024. #import pdb; pdb.set_trace()
  1025. t = float(self.max - self.min)
  1026. t /= self.cur - self.min
  1027. t = (t - 1) * self.elapsed
  1028. return '%s: %s' % (prefix, self.format_duration(t))
  1029. @property
  1030. def speed(self):
  1031. if self.elapsed == 0:
  1032. result = 0.0
  1033. else:
  1034. result = (self.cur - self.min) / self.elapsed
  1035. for unit in UNITS:
  1036. if result < 1000:
  1037. break
  1038. result /= 1000.0
  1039. return '%d %sB/s' % (result, unit)
  1040. #
  1041. # Glob functionality
  1042. #
  1043. RICH_GLOB = re.compile(r'\{([^}]*)\}')
  1044. _CHECK_RECURSIVE_GLOB = re.compile(r'[^/\\,{]\*\*|\*\*[^/\\,}]')
  1045. _CHECK_MISMATCH_SET = re.compile(r'^[^{]*\}|\{[^}]*$')
  1046. def iglob(path_glob):
  1047. """Extended globbing function that supports ** and {opt1,opt2,opt3}."""
  1048. if _CHECK_RECURSIVE_GLOB.search(path_glob):
  1049. msg = """invalid glob %r: recursive glob "**" must be used alone"""
  1050. raise ValueError(msg % path_glob)
  1051. if _CHECK_MISMATCH_SET.search(path_glob):
  1052. msg = """invalid glob %r: mismatching set marker '{' or '}'"""
  1053. raise ValueError(msg % path_glob)
  1054. return _iglob(path_glob)
  1055. def _iglob(path_glob):
  1056. rich_path_glob = RICH_GLOB.split(path_glob, 1)
  1057. if len(rich_path_glob) > 1:
  1058. assert len(rich_path_glob) == 3, rich_path_glob
  1059. prefix, set, suffix = rich_path_glob
  1060. for item in set.split(','):
  1061. for path in _iglob(''.join((prefix, item, suffix))):
  1062. yield path
  1063. else:
  1064. if '**' not in path_glob:
  1065. for item in std_iglob(path_glob):
  1066. yield item
  1067. else:
  1068. prefix, radical = path_glob.split('**', 1)
  1069. if prefix == '':
  1070. prefix = '.'
  1071. if radical == '':
  1072. radical = '*'
  1073. else:
  1074. # we support both
  1075. radical = radical.lstrip('/')
  1076. radical = radical.lstrip('\\')
  1077. for path, dir, files in os.walk(prefix):
  1078. path = os.path.normpath(path)
  1079. for fn in _iglob(os.path.join(path, radical)):
  1080. yield fn
  1081. #
  1082. # HTTPSConnection which verifies certificates/matches domains
  1083. #
  1084. class HTTPSConnection(httplib.HTTPSConnection):
  1085. ca_certs = None # set this to the path to the certs file (.pem)
  1086. check_domain = True # only used if ca_certs is not None
  1087. # noinspection PyPropertyAccess
  1088. def connect(self):
  1089. sock = socket.create_connection((self.host, self.port), self.timeout)
  1090. if getattr(self, '_tunnel_host', False):
  1091. self.sock = sock
  1092. self._tunnel()
  1093. if not hasattr(ssl, 'SSLContext'):
  1094. # For 2.x
  1095. if self.ca_certs:
  1096. cert_reqs = ssl.CERT_REQUIRED
  1097. else:
  1098. cert_reqs = ssl.CERT_NONE
  1099. self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file,
  1100. cert_reqs=cert_reqs,
  1101. ssl_version=ssl.PROTOCOL_SSLv23,
  1102. ca_certs=self.ca_certs)
  1103. else:
  1104. context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
  1105. context.options |= ssl.OP_NO_SSLv2
  1106. if self.cert_file:
  1107. context.load_cert_chain(self.cert_file, self.key_file)
  1108. kwargs = {}
  1109. if self.ca_certs:
  1110. context.verify_mode = ssl.CERT_REQUIRED
  1111. context.load_verify_locations(cafile=self.ca_certs)
  1112. if getattr(ssl, 'HAS_SNI', False):
  1113. kwargs['server_hostname'] = self.host
  1114. self.sock = context.wrap_socket(sock, **kwargs)
  1115. if self.ca_certs and self.check_domain:
  1116. try:
  1117. match_hostname(self.sock.getpeercert(), self.host)
  1118. logger.debug('Host verified: %s', self.host)
  1119. except CertificateError:
  1120. self.sock.shutdown(socket.SHUT_RDWR)
  1121. self.sock.close()
  1122. raise
  1123. class HTTPSHandler(BaseHTTPSHandler):
  1124. def __init__(self, ca_certs, check_domain=True):
  1125. BaseHTTPSHandler.__init__(self)
  1126. self.ca_certs = ca_certs
  1127. self.check_domain = check_domain
  1128. def _conn_maker(self, *args, **kwargs):
  1129. """
  1130. This is called to create a connection instance. Normally you'd
  1131. pass a connection class to do_open, but it doesn't actually check for
  1132. a class, and just expects a callable. As long as we behave just as a
  1133. constructor would have, we should be OK. If it ever changes so that
  1134. we *must* pass a class, we'll create an UnsafeHTTPSConnection class
  1135. which just sets check_domain to False in the class definition, and
  1136. choose which one to pass to do_open.
  1137. """
  1138. result = HTTPSConnection(*args, **kwargs)
  1139. if self.ca_certs:
  1140. result.ca_certs = self.ca_certs
  1141. result.check_domain = self.check_domain
  1142. return result
  1143. def https_open(self, req):
  1144. try:
  1145. return self.do_open(self._conn_maker, req)
  1146. except URLError as e:
  1147. if 'certificate verify failed' in str(e.reason):
  1148. raise CertificateError('Unable to verify server certificate '
  1149. 'for %s' % req.host)
  1150. else:
  1151. raise
  1152. #
  1153. # To prevent against mixing HTTP traffic with HTTPS (examples: A Man-In-The-
  1154. # Middle proxy using HTTP listens on port 443, or an index mistakenly serves
  1155. # HTML containing a http://xyz link when it should be https://xyz),
  1156. # you can use the following handler class, which does not allow HTTP traffic.
  1157. #
  1158. # It works by inheriting from HTTPHandler - so build_opener won't add a
  1159. # handler for HTTP itself.
  1160. #
  1161. class HTTPSOnlyHandler(HTTPSHandler, HTTPHandler):
  1162. def http_open(self, req):
  1163. raise URLError('Unexpected HTTP request on what should be a secure '
  1164. 'connection: %s' % req)
  1165. #
  1166. # XML-RPC with timeouts
  1167. #
  1168. _ver_info = sys.version_info[:2]
  1169. if _ver_info == (2, 6):
  1170. class HTTP(httplib.HTTP):
  1171. def __init__(self, host='', port=None, **kwargs):
  1172. if port == 0: # 0 means use port 0, not the default port
  1173. port = None
  1174. self._setup(self._connection_class(host, port, **kwargs))
  1175. class HTTPS(httplib.HTTPS):
  1176. def __init__(self, host='', port=None, **kwargs):
  1177. if port == 0: # 0 means use port 0, not the default port
  1178. port = None
  1179. self._setup(self._connection_class(host, port, **kwargs))
  1180. class Transport(xmlrpclib.Transport):
  1181. def __init__(self, timeout, use_datetime=0):
  1182. self.timeout = timeout
  1183. xmlrpclib.Transport.__init__(self, use_datetime)
  1184. def make_connection(self, host):
  1185. h, eh, x509 = self.get_host_info(host)
  1186. if _ver_info == (2, 6):
  1187. result = HTTP(h, timeout=self.timeout)
  1188. else:
  1189. if not self._connection or host != self._connection[0]:
  1190. self._extra_headers = eh
  1191. self._connection = host, httplib.HTTPConnection(h)
  1192. result = self._connection[1]
  1193. return result
  1194. class SafeTransport(xmlrpclib.SafeTransport):
  1195. def __init__(self, timeout, use_datetime=0):
  1196. self.timeout = timeout
  1197. xmlrpclib.SafeTransport.__init__(self, use_datetime)
  1198. def make_connection(self, host):
  1199. h, eh, kwargs = self.get_host_info(host)
  1200. if not kwargs:
  1201. kwargs = {}
  1202. kwargs['timeout'] = self.timeout
  1203. if _ver_info == (2, 6):
  1204. result = HTTPS(host, None, **kwargs)
  1205. else:
  1206. if not self._connection or host != self._connection[0]:
  1207. self._extra_headers = eh
  1208. self._connection = host, httplib.HTTPSConnection(h, None,
  1209. **kwargs)
  1210. result = self._connection[1]
  1211. return result
  1212. class ServerProxy(xmlrpclib.ServerProxy):
  1213. def __init__(self, uri, **kwargs):
  1214. self.timeout = timeout = kwargs.pop('timeout', None)
  1215. # The above classes only come into play if a timeout
  1216. # is specified
  1217. if timeout is not None:
  1218. scheme, _ = splittype(uri)
  1219. use_datetime = kwargs.get('use_datetime', 0)
  1220. if scheme == 'https':
  1221. tcls = SafeTransport
  1222. else:
  1223. tcls = Transport
  1224. kwargs['transport'] = t = tcls(timeout, use_datetime=use_datetime)
  1225. self.transport = t
  1226. xmlrpclib.ServerProxy.__init__(self, uri, **kwargs)
  1227. #
  1228. # CSV functionality. This is provided because on 2.x, the csv module can't
  1229. # handle Unicode. However, we need to deal with Unicode in e.g. RECORD files.
  1230. #
  1231. def _csv_open(fn, mode, **kwargs):
  1232. if sys.version_info[0] < 3:
  1233. mode += 'b'
  1234. else:
  1235. kwargs['newline'] = ''
  1236. return open(fn, mode, **kwargs)
  1237. class CSVBase(object):
  1238. defaults = {
  1239. 'delimiter': str(','), # The strs are used because we need native
  1240. 'quotechar': str('"'), # str in the csv API (2.x won't take
  1241. 'lineterminator': str('\n') # Unicode)
  1242. }
  1243. def __enter__(self):
  1244. return self
  1245. def __exit__(self, *exc_info):
  1246. self.stream.close()
  1247. class CSVReader(CSVBase):
  1248. def __init__(self, **kwargs):
  1249. if 'stream' in kwargs:
  1250. stream = kwargs['stream']
  1251. if sys.version_info[0] >= 3:
  1252. # needs to be a text stream
  1253. stream = codecs.getreader('utf-8')(stream)
  1254. self.stream = stream
  1255. else:
  1256. self.stream = _csv_open(kwargs['path'], 'r')
  1257. self.reader = csv.reader(self.stream, **self.defaults)
  1258. def __iter__(self):
  1259. return self
  1260. def next(self):
  1261. result = next(self.reader)
  1262. if sys.version_info[0] < 3:
  1263. for i, item in enumerate(result):
  1264. if not isinstance(item, text_type):
  1265. result[i] = item.decode('utf-8')
  1266. return result
  1267. __next__ = next
  1268. class CSVWriter(CSVBase):
  1269. def __init__(self, fn, **kwargs):
  1270. self.stream = _csv_open(fn, 'w')
  1271. self.writer = csv.writer(self.stream, **self.defaults)
  1272. def writerow(self, row):
  1273. if sys.version_info[0] < 3:
  1274. r = []
  1275. for item in row:
  1276. if isinstance(item, text_type):
  1277. item = item.encode('utf-8')
  1278. r.append(item)
  1279. row = r
  1280. self.writer.writerow(row)
  1281. #
  1282. # Configurator functionality
  1283. #
  1284. class Configurator(BaseConfigurator):
  1285. value_converters = dict(BaseConfigurator.value_converters)
  1286. value_converters['inc'] = 'inc_convert'
  1287. def __init__(self, config, base=None):
  1288. super(Configurator, self).__init__(config)
  1289. self.base = base or os.getcwd()
  1290. def configure_custom(self, config):
  1291. def convert(o):
  1292. if isinstance(o, (list, tuple)):
  1293. result = type(o)([convert(i) for i in o])
  1294. elif isinstance(o, dict):
  1295. if '()' in o:
  1296. result = self.configure_custom(o)
  1297. else:
  1298. result = {}
  1299. for k in o:
  1300. result[k] = convert(o[k])
  1301. else:
  1302. result = self.convert(o)
  1303. return result
  1304. c = config.pop('()')
  1305. if not callable(c):
  1306. c = self.resolve(c)
  1307. props = config.pop('.', None)
  1308. # Check for valid identifiers
  1309. args = config.pop('[]', ())
  1310. if args:
  1311. args = tuple([convert(o) for o in args])
  1312. items = [(k, convert(config[k])) for k in config if valid_ident(k)]
  1313. kwargs = dict(items)
  1314. result = c(*args, **kwargs)
  1315. if props:
  1316. for n, v in props.items():
  1317. setattr(result, n, convert(v))
  1318. return result
  1319. def __getitem__(self, key):
  1320. result = self.config[key]
  1321. if isinstance(result, dict) and '()' in result:
  1322. self.config[key] = result = self.configure_custom(result)
  1323. return result
  1324. def inc_convert(self, value):
  1325. """Default converter for the inc:// protocol."""
  1326. if not os.path.isabs(value):
  1327. value = os.path.join(self.base, value)
  1328. with codecs.open(value, 'r', encoding='utf-8') as f:
  1329. result = json.load(f)
  1330. return result
  1331. #
  1332. # Mixin for running