summaryrefslogtreecommitdiffstats
path: root/dhpython/interpreter.py
diff options
context:
space:
mode:
Diffstat (limited to 'dhpython/interpreter.py')
-rw-r--r--dhpython/interpreter.py576
1 files changed, 576 insertions, 0 deletions
diff --git a/dhpython/interpreter.py b/dhpython/interpreter.py
new file mode 100644
index 0000000..021e847
--- /dev/null
+++ b/dhpython/interpreter.py
@@ -0,0 +1,576 @@
+# Copyright © 2012-2013 Piotr Ożarowski <piotr@debian.org>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+import logging
+import os
+import re
+from os.path import exists, join, split
+from dhpython import INTERPRETER_DIR_TPLS, PUBLIC_DIR_RE, OLD_SITE_DIRS
+
+SHEBANG_RE = re.compile(r'''
+ (?:\#!\s*){0,1} # shebang prefix
+ (?P<path>
+ .*?/bin/.*?)?
+ (?P<name>
+ python|pypy)
+ (?P<version>
+ \d[\.\d]*)?
+ (?P<debug>
+ -dbg)?
+ (?P<options>.*)
+ ''', re.VERBOSE)
+EXTFILE_RE = re.compile(r'''
+ (?P<name>.*?)
+ (?:\.
+ (?P<stableabi>abi\d+)
+ |(?:\.
+ (?P<soabi>
+ (?P<impl>cpython|pypy)
+ -
+ (?P<ver>\d{2,})
+ (?P<flags>[a-z]*)
+ )?
+ (?:
+ (?:(?<!\.)-)? # minus sign only if soabi is defined
+ (?P<multiarch>[^/]*?)
+ )?
+ ))?
+ (?P<debug>_d)?
+ \.so$''', re.VERBOSE)
+log = logging.getLogger('dhpython')
+
+
+class Interpreter:
+ """
+ :attr path: /usr/bin/ in most cases
+ :attr name: pypy or python (even for python3 and python-dbg) or empty string
+ :attr version: interpreter's version
+ :attr debug: -dbg version of the interpreter
+ :attr impl: implementation (cpytho2, cpython3 or pypy)
+ :attr options: options parsed from shebang
+ :type path: str
+ :type name: str
+ :type version: Version or None
+ :type debug: bool
+ :type impl: str
+ :type options: tuple
+ """
+ path = '/usr/bin/'
+ name = 'python'
+ version = None
+ debug = False
+ impl = ''
+ options = ()
+ _cache = {}
+
+ def __init__(self, value=None, path=None, name=None, version=None,
+ debug=None, impl=None, options=None):
+ params = locals()
+ del params['self']
+ del params['value']
+
+ if isinstance(value, Interpreter):
+ for key in params.keys():
+ if params[key] is None:
+ params[key] = getattr(value, key)
+ elif value:
+ if value.replace('.', '').isdigit() and not version:
+ # version string
+ params['version'] = Version(value)
+ else:
+ # shebang or other string
+ for key, val in self.parse(value).items():
+ # prefer values passed to constructor over shebang ones:
+ if params[key] is None:
+ params[key] = val
+
+ for key, val in params.items():
+ if val is not None:
+ setattr(self, key, val)
+ elif key == 'version':
+ setattr(self, key, val)
+
+ def __setattr__(self, name, value):
+ if name == 'name':
+ if value not in ('python', 'pypy', ''):
+ raise ValueError("interpreter not supported: %s" % value)
+ if value == 'python':
+ if self.version:
+ if self.version.major == 3:
+ self.__dict__['impl'] = 'cpython3'
+ else:
+ self.__dict__['impl'] = 'cpython2'
+ elif value == 'pypy':
+ self.__dict__['impl'] = 'pypy'
+ elif name == 'version' and value is not None:
+ value = Version(value)
+ if not self.impl and self.name == 'python':
+ if value.major == 3:
+ self.impl = 'cpython3'
+ else:
+ self.impl = 'cpython2'
+ if name in ('path', 'name', 'impl', 'options') and value is None:
+ pass
+ elif name == 'debug':
+ self.__dict__[name] = bool(value)
+ else:
+ self.__dict__[name] = value
+
+ def __repr__(self):
+ result = self.path
+ if not result.endswith('/'):
+ result += '/'
+ result += self._vstr(self.version)
+ if self.options:
+ result += ' ' + ' '.join(self.options)
+ return result
+
+ def __str__(self):
+ return self._vstr(self.version)
+
+ def _vstr(self, version=None, consider_default_ver=False):
+ if self.impl == 'pypy':
+ # TODO: will Debian support more than one PyPy version?
+ return self.name
+ version = version or self.version or ''
+ if consider_default_ver and (not version or version == self.default_version):
+ version = '3' if self.impl == 'cpython3' else '2'
+ if self.debug:
+ return 'python{}-dbg'.format(version)
+ return self.name + str(version)
+
+ def binary(self, version=None):
+ return '{}{}'.format(self.path, self._vstr(version))
+
+ @property
+ def binary_dv(self):
+ """Like binary(), but returns path to default intepreter symlink
+ if version matches default one for given implementation.
+ """
+ return '{}{}'.format(self.path, self._vstr(consider_default_ver=True))
+
+ @property
+ def default_version(self):
+ if self.impl:
+ return default(self.impl)
+
+ @staticmethod
+ def parse(shebang):
+ """Return dict with parsed shebang
+
+ >>> sorted(Interpreter.parse('/usr/bin/python3.2-dbg').items())
+ [('debug', '-dbg'), ('name', 'python'), ('options', ()), ('path', '/usr/bin/'), ('version', '3.2')]
+ >>> sorted(Interpreter.parse('#! /usr/bin/python3.2').items())
+ [('debug', None), ('name', 'python'), ('options', ()), ('path', '/usr/bin/'), ('version', '3.2')]
+ >>> sorted(Interpreter.parse('/usr/bin/python3.2-dbg --foo --bar').items())
+ [('debug', '-dbg'), ('name', 'python'), ('options', ('--foo', '--bar')),\
+ ('path', '/usr/bin/'), ('version', '3.2')]
+ """
+ result = SHEBANG_RE.search(shebang)
+ if not result:
+ return {}
+ result = result.groupdict()
+ if 'options' in result:
+ # TODO: do we need "--key value" here?
+ result['options'] = tuple(result['options'].split())
+ if result['name'] == 'python' and result['version'] is None:
+ result['version'] = '2'
+ return result
+
+ @classmethod
+ def from_file(cls, fpath):
+ """Read file's shebang and parse it."""
+ interpreter = Interpreter()
+ with open(fpath, 'rb') as fp:
+ data = fp.read(96)
+ if b"\0" in data:
+ raise ValueError('cannot parse binary file')
+ # make sure only first line is checkeed
+ data = str(data, 'utf-8').split('\n')[0]
+ if not data.startswith('#!'):
+ raise ValueError("doesn't look like a shebang: %s" % data)
+
+ parsed = cls.parse(data)
+ if not parsed:
+ raise ValueError("doesn't look like a shebang: %s" % data)
+ for key, val in parsed.items():
+ setattr(interpreter, key, val)
+ return interpreter
+
+ def sitedir(self, package=None, version=None, gdb=False):
+ """Return path to site-packages directory.
+
+ Note that returned path is not the final location of .py files
+
+ >>> i = Interpreter('python')
+ >>> i.sitedir(version='3.1')
+ '/usr/lib/python3/dist-packages/'
+ >>> i.sitedir(version='2.5')
+ '/usr/lib/python2.5/site-packages/'
+ >>> i.sitedir(version=Version('2.7'))
+ '/usr/lib/python2.7/dist-packages/'
+ >>> i.sitedir(version='3.1', gdb=True, package='python3-foo')
+ 'debian/python3-foo/usr/lib/debug/usr/lib/python3/dist-packages/'
+ >>> i.sitedir(version=Version('3.2'))
+ '/usr/lib/python3/dist-packages/'
+ """
+ try:
+ version = Version(version or self.version)
+ except Exception as err:
+ raise ValueError("cannot find valid version: %s" % err)
+ if self.impl == 'pypy':
+ path = '/usr/lib/pypy/dist-packages/'
+ elif version << Version('2.6'):
+ path = "/usr/lib/python%s/site-packages/" % version
+ elif version << Version('3.0'):
+ path = "/usr/lib/python%s/dist-packages/" % version
+ else:
+ path = '/usr/lib/python3/dist-packages/'
+
+ if gdb:
+ path = "/usr/lib/debug%s" % path
+ if package:
+ path = "debian/%s%s" % (package, path)
+
+ return path
+
+ def old_sitedirs(self, package=None, version=None, gdb=False):
+ """Return deprecated paths to site-packages directories."""
+ try:
+ version = Version(version or self.version)
+ except Exception as err:
+ raise ValueError("cannot find valid version: %s" % err)
+ result = []
+ for item in OLD_SITE_DIRS.get(self.impl, []):
+ if isinstance(item, str):
+ result.append(item.format(version))
+ else:
+ res = item(version)
+ if res is not None:
+ result.append(res)
+
+ if gdb:
+ result = ['/usr/lib/debug{}'.format(i) for i in result]
+ if self.impl.startswith('cpython'):
+ result.append('/usr/lib/debug/usr/lib/pyshared/python{}'.format(version))
+ if package:
+ result = ['debian/{}{}'.format(package, i) for i in result]
+
+ return result
+
+ def parse_public_dir(self, path):
+ """Return version assigned to site-packages path
+ or True is it's unversioned public dir."""
+ match = PUBLIC_DIR_RE[self.impl].match(path)
+ if match:
+ vers = match.groups(0)
+ if vers and vers[0]:
+ return Version(vers)
+ return True
+
+ def should_ignore(self, path):
+ """Return True if path is used by another interpreter implementation."""
+ cache_key = 'should_ignore_{}'.format(self.impl)
+ if cache_key not in self.__class__._cache:
+ expr = [v for k, v in INTERPRETER_DIR_TPLS.items() if k != self.impl]
+ regexp = re.compile('|'.join('({})'.format(i) for i in expr))
+ self.__class__._cache[cache_key] = regexp
+ else:
+ regexp = self.__class__._cache[cache_key]
+ return regexp.search(path)
+
+ def cache_file(self, fpath, version=None):
+ """Given path to a .py file, return path to its .pyc/.pyo file.
+
+ This function is inspired by Python 3.2's imp.cache_from_source.
+
+ :param fpath: path to file name
+ :param version: Python version
+
+ >>> i = Interpreter('python')
+ >>> i.cache_file('foo.py', Version('3.1'))
+ 'foo.pyc'
+ >>> i.cache_file('bar/foo.py', '3.8') # doctest: +SKIP
+ 'bar/__pycache__/foo.cpython-38.pyc'
+ """
+ version = Version(version or self.version)
+ last_char = 'o' if '-O' in self.options else 'c'
+ if version <= Version('3.1'):
+ return fpath + last_char
+
+ fdir, fname = split(fpath)
+ if not fname.endswith('.py'):
+ fname += '.py'
+ return join(fdir, '__pycache__', "%s.%s.py%s" %
+ (fname[:-3], self.magic_tag(version), last_char))
+
+ def magic_number(self, version=None):
+ """Return magic number."""
+ version = Version(version or self.version)
+ if self.impl == 'cpython2':
+ return ''
+ result = self._execute('import imp; print(imp.get_magic())', version)
+ return eval(result)
+
+ def magic_tag(self, version=None):
+ """Return Python magic tag (used in __pycache__ dir to tag files).
+
+ >>> i = Interpreter('python')
+ >>> i.magic_tag(version='3.8') # doctest: +SKIP
+ 'cpython-38'
+ """
+ version = Version(version or self.version)
+ if self.impl.startswith('cpython') and version << Version('3.2'):
+ return ''
+ return self._execute('import imp; print(imp.get_tag())', version)
+
+ def multiarch(self, version=None):
+ """Return multiarch tag."""
+ version = Version(version or self.version)
+ try:
+ soabi, multiarch = self._get_config(version)[:2]
+ except Exception:
+ log.debug('cannot get multiarch', exc_info=True)
+ # interpreter without multiarch support
+ return ''
+ return multiarch
+
+ def stableabi(self, version=None):
+ version = Version(version or self.version)
+ # stable ABI was introduced in Python 3.3
+ if self.impl == 'cpython3' and version >> Version('3.2'):
+ return 'abi{}'.format(version.major)
+
+ def soabi(self, version=None):
+ """Return SOABI flag (used to in .so files)."""
+ version = Version(version or self.version)
+ # NOTE: it's not the same as magic_tag
+ try:
+ soabi, multiarch = self._get_config(version)[:2]
+ except Exception:
+ log.debug('cannot get soabi', exc_info=True)
+ # interpreter without soabi support
+ return ''
+ return soabi
+
+ @property
+ def include_dir(self):
+ """Return INCLUDE_DIR path.
+
+ >>> Interpreter('python2.7').include_dir # doctest: +SKIP
+ '/usr/include/python2.7'
+ >>> Interpreter('python3.8-dbg').include_dir # doctest: +SKIP
+ '/usr/include/python3.8d'
+ """
+ if self.impl == 'pypy':
+ return '/usr/lib/pypy/include'
+ try:
+ result = self._get_config()[2]
+ if result:
+ return result
+ except Exception:
+ result = ''
+ log.debug('cannot get include path', exc_info=True)
+ result = '/usr/include/{}'.format(self.name)
+ version = self.version
+ if self.debug:
+ if version >= '3.8':
+ result += 'd'
+ elif version << '3.3':
+ result += '_d'
+ else:
+ result += 'dm'
+ else:
+ if version >= '3.8':
+ pass
+ elif version >> '3.2':
+ result += 'm'
+ elif version == '3.2':
+ result += 'mu'
+ return result
+
+ @property
+ def symlinked_include_dir(self):
+ """Return path to symlinked include directory."""
+ if self.impl in ('cpython2', 'pypy') or self.debug \
+ or self.version >> '3.7' or self.version << '3.3':
+ # these interpreters do not provide symlink,
+ # others provide it in libpython3.X-dev
+ return
+ try:
+ result = self._get_config()[2]
+ if result:
+ if result.endswith('m'):
+ return result[:-1]
+ else:
+ # there's include_dir, but no "m"
+ return
+ except Exception:
+ result = '/usr/include/{}'.format(self.name)
+ log.debug('cannot get include path', exc_info=True)
+ return result
+
+ @property
+ def library_file(self):
+ """Return libfoo.so file path."""
+ if self.impl == 'pypy':
+ return ''
+ libpl, ldlibrary = self._get_config()[3:5]
+ if ldlibrary.endswith('.a'):
+ # python3.1-dbg, python3.2, python3.2-dbg returned static lib
+ ldlibrary = ldlibrary.replace('.a', '.so')
+ if libpl and ldlibrary:
+ return join(libpl, ldlibrary)
+ raise Exception('cannot find library file for {}'.format(self))
+
+ def check_extname(self, fname, version=None):
+ """Return extension file name if file can be renamed."""
+ if not version and not self.version:
+ return
+
+ version = Version(version or self.version)
+
+ if '/' in fname:
+ fdir, fname = fname.rsplit('/', 1) # in case full path was passed
+ else:
+ fdir = ''
+
+ info = EXTFILE_RE.search(fname)
+ if not info:
+ return
+ info = info.groupdict()
+ if info['ver'] and (not version or version.minor is None):
+ # get version from soabi if version is not set of only major
+ # version number is set
+ version = Version("%s.%s" % (info['ver'][0], info['ver'][1]))
+
+ if info['stableabi']:
+ # files with stable ABI in name don't need changes
+ return
+ if info['debug'] and self.debug is False:
+ # do not change Python 2.X extensions already marked as debug
+ # (the other way around is acceptable)
+ return
+ if info['soabi'] and info['multiarch']:
+ # already tagged, nothing we can do here
+ return
+
+ try:
+ soabi, multiarch = self._get_config(version)[:2]
+ except Exception:
+ log.debug('cannot get soabi/multiarch', exc_info=True)
+ return
+
+ if info['soabi'] and soabi and info['soabi'] != soabi:
+ return
+
+ tmp_soabi = info['soabi'] or soabi
+ tmp_multiarch = info['multiarch'] or multiarch
+
+ result = info['name']
+ if result.endswith('module') and result != 'module' and (
+ self.impl == 'cpython3' and version >> '3.2' or
+ self.impl == 'cpython2' and version == '2.7'):
+ result = result[:-6]
+
+ if tmp_soabi:
+ result = "{}.{}".format(result, tmp_soabi)
+ if tmp_multiarch and not (self.impl == 'cpython3' and version << '3.3') and tmp_multiarch not in soabi:
+ result = "{}-{}".format(result, tmp_multiarch)
+ elif self.impl == 'cpython2' and version == '2.7' and tmp_multiarch:
+ result = "{}.{}".format(result, tmp_multiarch)
+
+ if self.debug and self.impl == 'cpython2':
+ result += '_d'
+ result += '.so'
+ if fname == result:
+ return
+ return join(fdir, result)
+
+ def suggest_pkg_name(self, name):
+ """Suggest binary package name with for given library name
+
+ >>> Interpreter('python3.1').suggest_pkg_name('foo')
+ 'python3-foo'
+ >>> Interpreter('python3.8').suggest_pkg_name('foo_bar')
+ 'python3-foo-bar'
+ >>> Interpreter('python2.7-dbg').suggest_pkg_name('bar')
+ 'python-bar-dbg'
+ """
+ name = name.replace('_', '-')
+ if self.impl == 'pypy':
+ return 'pypy-{}'.format(name)
+ version = '3' if self.impl == 'cpython3' else ''
+ result = 'python{}-{}'.format(version, name)
+ if self.debug:
+ result += '-dbg'
+ return result
+
+ def _get_config(self, version=None):
+ version = Version(version or self.version)
+ # sysconfig module is available since Python 3.2
+ # (also backported to Python 2.7)
+ if self.impl == 'pypy' or self.impl.startswith('cpython') and (
+ version >> '2.6' and version << '3'
+ or version >> '3.1' or version == '3'):
+ cmd = 'import sysconfig as s;'
+ else:
+ cmd = 'from distutils import sysconfig as s;'
+ cmd += 'print("__SEP__".join(i or "" ' \
+ 'for i in s.get_config_vars('\
+ '"SOABI", "MULTIARCH", "INCLUDEPY", "LIBPL", "LDLIBRARY")))'
+ conf_vars = self._execute(cmd, version).split('__SEP__')
+ if conf_vars[1] in conf_vars[0]:
+ # Python >= 3.5 includes MILTIARCH in SOABI
+ conf_vars[0] = conf_vars[0].replace("-%s" % conf_vars[1], '')
+ try:
+ conf_vars[1] = os.environ['DEB_HOST_MULTIARCH']
+ except KeyError:
+ pass
+ return conf_vars
+
+ def _execute(self, command, version=None, cache=True):
+ version = Version(version or self.version)
+ exe = "{}{}".format(self.path, self._vstr(version))
+ command = "{} -c '{}'".format(exe, command.replace("'", "\'"))
+ if cache and command in self.__class__._cache:
+ return self.__class__._cache[command]
+ if not exists(exe):
+ raise Exception("cannot execute command due to missing "
+ "interpreter: %s" % exe)
+
+ output = execute(command)
+ if output['returncode'] != 0:
+ log.debug(output['stderr'])
+ raise Exception('{} failed with status code {}'.format(command, output['returncode']))
+
+ result = output['stdout'].splitlines()
+
+ if len(result) == 1:
+ result = result[0]
+
+ if cache:
+ self.__class__._cache[command] = result
+
+ return result
+
+# due to circular imports issue
+from dhpython.tools import execute
+from dhpython.version import Version, default