Linux ip-172-26-7-228 5.4.0-1103-aws #111~18.04.1-Ubuntu SMP Tue May 23 20:04:10 UTC 2023 x86_64
Your IP : 18.119.253.198
# 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 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 ''
elif isinstance(version, Version) and version == Version(major=2):
version = '' # do not promote /usr/bin/python2
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.4')
'bar/__pycache__/foo.cpython-34.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.4')
'cpython-34'
"""
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
'/usr/include/python2.7'
>>> Interpreter('python3.4-dbg').include_dir
'/usr/include/python3.4dm'
"""
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.3':
result += '_d'
else:
result += 'dm'
else:
if version >> '3.2':
result += 'm'
elif version == '3.2':
result += 'mu'
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 arround 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 (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.4').suggest_pkg_name('foo')
'python3-foo'
>>> Interpreter('python2.7-dbg').suggest_pkg_name('bar')
'python-bar-dbg'
"""
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)
command = "{} -c '{}'".format(self._vstr(version), command.replace("'", "\'"))
if cache and command in self.__class__._cache:
return self.__class__._cache[command]
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
|