feat: add various system and theme icons, configuration files, and support assets

This commit is contained in:
jschantli committed 2026-10-03 08:14:42 -07:00
1 parent 848dac4264
commit 54baea79f4
15893 files changed
+781426 -2

No files matched your search

@@ -0,0 +1,12 @@
[DEFAULT]
# the default python3 version
default-version = python3.12
# all supported python3 versions
supported-versions = python3.12
# formerly supported python3 versions
old-versions = python3.1, python3.2, python3.3, python3.4, python3.5, python3.6, python3.7, python3.8, python3.9, python3.10, python3.11
# unsupported versions, including older versions
unsupported-versions = python3.1, python3.2, python3.3, python3.4, python3.5, python3.6, python3.7, python3.8, python3.9, python3.10, python3.11
@@ -0,0 +1,58 @@
try:
from datetime import datetime
except ImportError:
datetime = None
import logging
import re
from subprocess import PIPE, Popen
from pickle import dumps
log = logging.getLogger(__name__)
PUBLIC_DIR_RE = re.compile(r'.*?/usr/lib/python(\d(?:.\d+)?)/(site|dist)-packages')
class memoize:
def __init__(self, func):
self.func = func
self.cache = {}
def __call__(self, *args, **kwargs):
key = dumps((args, kwargs))
if key not in self.cache:
self.cache[key] = self.func(*args, **kwargs)
return self.cache[key]
def execute(command, cwd=None, env=None, log_output=None):
"""Execute external commad.
:param cdw: currennt working directory
:param env: environment
:param log_output:
* opened log file or path to this file, or
* None if output should be included in the returned dict, or
* False if output should be redirectored to stdout/stderr
"""
args = {'shell': False, 'cwd': cwd, 'env': env}
close = False
if log_output is False:
pass
elif log_output is None:
args.update(stdout=PIPE, stderr=PIPE)
elif log_output:
if isinstance(log_output, str):
close = True
log_output = open(log_output, 'a')
if datetime:
log_output.write('\n# command executed on {}'.format(datetime.now().isoformat()))
log_output.write('\n$ {}\n'.format(repr(command)))
log_output.flush()
args.update(stdout=log_output, stderr=log_output)
log.debug('invoking: %r', command)
with Popen(command, **args) as process:
stdout, stderr = process.communicate()
close and log_output.close()
return dict(returncode=process.returncode,
stdout=stdout and str(stdout, 'utf-8'),
stderr=stderr and str(stderr, 'utf-8'))
@@ -0,0 +1,86 @@
# Copyright © 2012 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
from os import walk, environ
from os.path import abspath, isfile, join
from subprocess import Popen, PIPE
from debpython import PUBLIC_DIR_RE
log = logging.getLogger(__name__)
def from_directory(dname, extensions=('.py',)):
"""Generate *.py file names available in given directory."""
extensions = tuple(extensions) # .endswith doesn't like list
if isinstance(dname, (list, tuple)):
for item in dname:
for fn in from_directory(item):
yield fn
elif isfile(dname) and dname.endswith(extensions):
yield dname
else:
for root, dirs, file_names in walk(abspath(dname)):
for fn in file_names:
if fn.endswith(extensions):
yield join(root, fn)
def from_package(package_name, extensions=('.py',)):
"""Generate *.py file names available in given package."""
extensions = tuple(extensions) # .endswith doesn't like list
env = environ.copy()
env['LC_ALL'] = 'C.UTF-8'
process = Popen(('/usr/bin/dpkg', '-L', package_name), stdout=PIPE,
stderr=PIPE, env=env)
stdout, stderr = process.communicate()
if process.returncode != 0:
raise Exception("cannot get content of %s" % package_name)
stdout = stdout.decode('utf-8', errors='replace')
for line in stdout.splitlines():
if line.endswith(extensions):
yield line
def filter_directory(files, dname):
"""Generate *.py file names that match given directory."""
for fn in files:
if fn.startswith(dname):
yield fn
def filter_public(files, versions):
"""Generate *.py file names that match given versions."""
vstr = set("%d.%d" % i for i in versions)
shared_vstr = set(str(i[0]) for i in versions)
for fn in files:
public_dir = PUBLIC_DIR_RE.match(fn)
if public_dir:
vers = public_dir.group(1)
if vers in shared_vstr or vers in vstr:
yield fn
def filter_out_ext(files, extensions):
"""Removes files with matching extensions from given generator."""
extensions = tuple(extensions) # .endswith doesn't like list
for fn in files:
if not fn.endswith(extensions):
yield fn
@@ -0,0 +1,369 @@
# Copyright © 2012 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 debpython import execute
from debpython.version import Version
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]*?)
)
(?:
-(?P<multiarch>[^/]*?)
)?
))?
(?P<debug>_d)?
\.so$''', re.VERBOSE)
log = logging.getLogger(__name__)
class Interpreter:
path = None
name = 'python'
version = None
debug = False
impl = 'cpython' # implementation
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:
params['version'] = Version(value)
else:
for key, val in self.parse(value).items():
if params[key] is None:
params[key] = val
for key, val in params.items():
setattr(self, key, val)
def __setattr__(self, name, value):
if name == 'name' and value:
if value.startswith('python'):
self.__dict__['impl'] = 'cpython'
elif value.startswith('pypy'):
self.__dict__['impl'] = 'pypy'
if '-dbg' in value:
self.__dict__['debug'] = True
elif name == 'version' and value is not None:
value = Version(value)
if name in ('name', 'impl', 'debug', 'options') and value is None:
# get the class default instead
self.__dict__[name] = getattr(Interpreter, name)
else:
self.__dict__[name] = value
def __repr__(self):
result = self.path or ''
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):
if self.impl == 'pypy':
# TODO: will Debian support more than one PyPy version?
return self.name
if version and str(version) not in self.name:
if self.debug:
return 'python{}-dbg'.format(version)
return self.name + str(version)
return self.name
@staticmethod
def parse(shebang):
"""Return dict with parsed shebang
>>> sorted(Interpreter.parse('pypy').items())
[('debug', None), ('name', 'pypy'), ('options', ()), ('path', None), ('version', None)]
>>> sorted(Interpreter.parse('/usr/bin/python3.3-dbg').items())
[('debug', '-dbg'), ('name', 'python'), ('options', ()), ('path', '/usr/bin/'), ('version', '3.3')]
>>> sorted(Interpreter.parse('#! /usr/bin/pypy --foo').items())
[('debug', None), ('name', 'pypy'), ('options', ('--foo',)), ('path', '/usr/bin/'), ('version', None)]
>>> 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())
return result
def sitedir(self, gdb=False, package=None, version=None):
"""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/'
>>> Interpreter('pypy').sitedir(version='2.0')
'/usr/lib/pypy/dist-packages/'
"""
version = Version(version or self.version)
#if not version:
# version = Version(DEFAULT)
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 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.2')
'bar/__pycache__/foo.cpython-32.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 ext_file(self, name, version=None):
"""Return extension name with soname and multiarch tags."""
version = Version(version or self.version)
soabi, multiarch = self._get_config(version)
result = name.split('.', 1)[0]
if soabi:
result += '.{}'.format(soabi)
if multiarch:
result += '-{}'.format(multiarch)
if self.debug and self.impl == 'cpython'\
and version << Version('3'):
result += '_d'
return '{}.so'.format(result)
def magic_number(self, version=None):
"""Return magic number."""
version = Version(version or self.version)
if self.impl == 'cpython' and version << Version('3'):
return ''
result = self._execute('import importlib.util; print(importlib.util.MAGIC_NUMBER)', 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.2')
'cpython-32'
"""
version = Version(version or self.version)
if self.impl == 'cpython' and version << Version('3.2'):
return ''
return self._execute('import sys; print(sys.implementation.cache_tag)', version)
def multiarch(self, version=None):
"""Return multiarch tag."""
version = Version(version or self.version)
try:
soabi, multiarch = self._get_config(version)
except Exception:
log.debug('cannot get multiarch', exc_info=True)
# interpreter without multiach 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 == 'cpython' and version >> Version('3.2'):
return 'abi{}'.format(version.major)
def soabi(self, version=None):
"""Return SOABI flag (used to in .so files).
>>> i = Interpreter('python')
>>> i.soabi(version=Version('3.3'))
'cpython-33m'
"""
version = Version(version or self.version)
# NOTE: it's not the same as magic_tag
try:
soabi, multiarch = self._get_config(version)
except Exception:
log.debug('cannot get soabi', exc_info=True)
# interpreter without soabi support
return ''
return soabi
def check_extname(self, fname, version=None):
"""Return extension file name if file can be renamed.
>>> i = Interpreter('python')
>>> i.check_extname('foo.so', version='3.3') # doctest: +ELLIPSIS
'foo.cpython-33m-....so'
>>> i.check_extname('foo.abi3.so', version='3.3')
>>> i.check_extname('foo/bar/bazmodule.so', version='3.3') # doctest: +ELLIPSIS
'foo/bar/baz.cpython-33m-....so'
"""
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['stableabi']:
# files with stable ABI in name don't need changes
return
i = Interpreter(self, version=version)
if info['ver']:
i.version = "{}.{}".format(info['ver'][0], info['ver'][1])
if not i.debug and (info['debug'] or 'd' in (info['flags'] or '')):
i.debug = True
try:
soabi, multiarch = i._get_config()
except Exception:
log.debug('cannot get soabi/multiarch', exc_info=True)
return
result = info['name']
if i.impl == 'cpython' and i.version >> '3.2' and result.endswith('module'):
result = result[:-6]
if info['soabi'] or soabi:
result = "{}.{}".format(result, info['soabi'] or soabi)
if info['multiarch'] or multiarch:
result = "{}-{}".format(result, info['multiarch'] or multiarch)
result += '.so'
if fname == result:
return
return join(fdir, 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 == '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")))'
conf_vars = self._execute(cmd, version).split('__SEP__')
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 = (self._vstr(version), "-c", command)
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
@@ -0,0 +1,47 @@
# -*- coding: UTF-8 -*-
# Copyright © 2010-2012 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 re
import optparse
from copy import copy
from debpython.version import parse_vrange
def parse_version_range(option, opt, value):
try:
return parse_vrange(value)
except ValueError:
raise optparse.OptionValueError("version range is invalid: %s" % value)
def compile_regexpr(option, opt, value):
try:
pattern = re.compile(value)
except Exception:
raise optparse.OptionValueError('regular expression is not valid')
return pattern
class Option(optparse.Option):
TYPES = optparse.Option.TYPES + ('version_range', 'regexpr')
TYPE_CHECKER = copy(optparse.Option.TYPE_CHECKER)
TYPE_CHECKER['version_range'] = parse_version_range
TYPE_CHECKER['regexpr'] = compile_regexpr
@@ -0,0 +1,459 @@
# -*- coding: UTF-8 -*-
# Copyright © 2010-2012 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 re
from os import environ
from os.path import exists
from configparser import ConfigParser
from types import GeneratorType
RANGE_PATTERN = r'(-)?(\d\.\d+)(?:(-)(\d\.\d+)?)?'
RANGE_RE = re.compile(RANGE_PATTERN)
VERSION_RE = re.compile(r'''
(?P<major>\d+)\.?
(?P<minor>\d+)?\.?
(?P<micro>\d+)?[.\s]?
(?P<releaselevel>alpha|beta|candidate|final)?[.\s]?
(?P<serial>\d+)?''', re.VERBOSE)
log = logging.getLogger(__name__)
# try to read debian_defaults and get a list of supported Python versions and
# the default one from there
_supported = environ.get('DEBPYTHON3_SUPPORTED')
_default = environ.get('DEBPYTHON3_DEFAULT')
if not _supported or not _default:
_config = ConfigParser()
_config.read('/usr/share/python3/debian_defaults')
if not _default:
_default = _config.get('DEFAULT', 'default-version')[6:]
if not _supported:
_supported = _config.get('DEFAULT', 'supported-versions')\
.replace('python', '')
try:
DEFAULT = tuple(int(i) for i in _default.split('.'))
except Exception:
log.exception('cannot read debian_defaults')
try:
SUPPORTED = tuple(tuple(int(j) for j in i.strip().split('.'))
for i in _supported.split(','))
except Exception:
log.exception('cannot read debian_defaults')
class Version:
def __init__(self, value=None, major=None, minor=None, micro=None,
releaselevel=None, serial=None):
if isinstance(value, (tuple, list)):
value = '.'.join(str(i) for i in value)
if isinstance(value, Version):
for name in ('major', 'minor', 'micro', 'releaselevel', 'serial'):
setattr(self, name, getattr(value, name))
return
comp = locals()
del comp['self']
del comp['value']
if value:
match = VERSION_RE.match(value)
for name, value in match.groupdict().items() if match else []:
if value is not None and comp[name] is None:
comp[name] = value
for name, value in comp.items():
if name != 'releaselevel' and value is not None:
value = int(value)
setattr(self, name, value)
if not self.major:
raise ValueError('major component is required')
def __str__(self):
"""Return major.minor or major string.
>>> str(Version(major=3, minor=2, micro=1, releaselevel='final', serial=4))
'3.2'
>>> str(Version(major=2))
'2'
"""
result = str(self.major)
if self.minor is not None:
result += '.{}'.format(self.minor)
return result
def __hash__(self):
return hash(repr(self))
def __repr__(self):
"""Return full version string.
>>> repr(Version(major=3, minor=2, micro=1, releaselevel='final', serial=4))
"Version('3.2.1.final.4')"
>>> repr(Version(major=2))
"Version('2')"
"""
result = "Version('{}".format(self)
for name in ('micro', 'releaselevel', 'serial'):
value = getattr(self, name)
if not value:
break
result += '.{}'.format(value)
return result + "')"
def __add__(self, other):
"""Return next version.
>>> Version('3.1') + 1
Version('3.2')
>>> Version('2') + '1'
Version('3')
"""
result = Version(self)
if self.minor is None:
result.major += int(other)
else:
result.minor += int(other)
return result
def __sub__(self, other):
"""Return previous version.
>>> Version('3.1') - 1
Version('3.0')
>>> Version('3') - '1'
Version('2')
"""
result = Version(self)
if self.minor is None:
result.major -= int(other)
new = result.major
else:
result.minor -= int(other)
new = result.minor
if new < 0:
raise ValueError('cannot decrease version further')
return result
def __eq__(self, other):
return self.__cmp(other) == 0
def __lt__(self, other):
return self.__cmp(other) < 0
def __le__(self, other):
return self.__cmp(other) <= 0
def __gt__(self, other):
return self.__cmp(other) > 0
def __ge__(self, other):
return self.__cmp(other) >= 0
def __lshift__(self, other):
"""Compare major.minor or major only (if minor is not set).
>>> Version('2.6') << Version('2.7')
True
>>> Version('2.6') << Version('2.6.6')
False
>>> Version('3') << Version('2')
False
>>> Version('3.1') << Version('2')
False
>>> Version('2') << Version('3.2.1.alpha.3')
True
"""
if not isinstance(other, Version):
other = Version(other)
if self.minor is None or other.minor is None:
return self.__cmp(other, ignore='minor') < 0
else:
return self.__cmp(other, ignore='micro') < 0
def __rshift__(self, other):
"""Compare major.minor or major only (if minor is not set).
>>> Version('2.6') >> Version('2.7')
False
>>> Version('2.6.7') >> Version('2.6.6')
False
>>> Version('3') >> Version('2')
True
>>> Version('3.1') >> Version('2')
True
>>> Version('2.1') >> Version('3.2.1.alpha.3')
False
"""
if not isinstance(other, Version):
other = Version(other)
if self.minor is None or other.minor is None:
return self.__cmp(other, ignore='minor') > 0
else:
return self.__cmp(other, ignore='micro') > 0
def __cmp(self, other, ignore=None):
if not isinstance(other, Version):
other = Version(other)
for name in ('major', 'minor', 'micro', 'releaselevel', 'serial'):
if name == ignore:
break
value1 = getattr(self, name) or 0
value2 = getattr(other, name) or 0
if name == 'releaselevel':
rmap = {'alpha': -3, 'beta': -2, 'candidate': -1, 'final': 0}
value1 = rmap.get(value1, 0)
value2 = rmap.get(value2, 0)
if value1 == value2:
continue
return (value1 > value2) - (value1 < value2)
return 0
def get_requested_versions(vrange=None, available=None):
"""Return a set of requested and supported Python versions.
:param available: if set to `True`, return installed versions only,
if set to `False`, return requested versions that are not installed.
By default returns all requested versions.
:type available: bool
>>> sorted(get_requested_versions([(3, 0), None]))
[(3, 2), (3, 3)]
>>> sorted(get_requested_versions('')) == sorted(SUPPORTED)
True
>>> sorted(get_requested_versions([None, None])) == sorted(SUPPORTED)
True
>>> get_requested_versions([(5, 0), None]) == set()
True
"""
if isinstance(vrange, str):
vrange = parse_vrange(vrange)
if not vrange or list(vrange) == [None, None]:
versions = set(SUPPORTED)
else:
minv = (0, 0) if vrange[0] is None else vrange[0]
maxv = (99, 99) if vrange[1] is None else vrange[1]
if minv == maxv:
versions = set((minv,) if minv in SUPPORTED else tuple())
else:
versions = set(v for v in SUPPORTED if minv <= v < maxv)
if available:
versions = set(v for v in versions
if exists("/usr/bin/python%d.%d" % v))
elif available is False:
versions = set(v for v in versions
if not exists("/usr/bin/python%d.%d" % v))
return versions
def parse_vrange(value):
"""Return minimum and maximum Python version from given range.
>>> parse_vrange('3.0-')
((3, 0), None)
>>> parse_vrange('3.1-3.3')
((3, 1), (3, 3))
>>> parse_vrange('3.2-4.0')
((3, 2), (4, 0))
>>> parse_vrange('-3.7')
(None, (3, 7))
>>> parse_vrange('3.2')
((3, 2), (3, 2))
>>> parse_vrange('') == parse_vrange('-') == (None, None)
True
"""
if value in ('', '-'):
return None, None
match = RANGE_RE.match(value)
if not match:
raise ValueError("version range is invalid: %s" % value)
groups = match.groups()
if list(groups).count(None) == 3: # only one version is allowed
minv = tuple(int(i) for i in groups[1].split('.'))
return minv, minv
minv = maxv = None
if groups[0]: # maximum version only
maxv = groups[1]
else:
minv = groups[1]
maxv = groups[3]
minv = tuple(int(i) for i in minv.split('.')) if minv else None
maxv = tuple(int(i) for i in maxv.split('.')) if maxv else None
if maxv and minv and minv > maxv:
raise ValueError("version range is invalid: %s" % value)
return minv, maxv
def parse_pycentral_vrange(value):
"""Parse X-Python3-Version.
>>> parse_pycentral_vrange('>= 3.1')
((3, 1), None)
>>> parse_pycentral_vrange('<< 4.0')
(None, (4, 0))
>>> parse_pycentral_vrange('3.1')
((3, 1), (3, 1))
>>> parse_pycentral_vrange('3.1, 3.2')
((3, 1), None)
"""
#get = lambda x: get_requested_versions(parse_vrange(x))
minv = maxv = None
hardcoded = set()
for item in value.split(','):
item = item.strip()
match = re.match(r'>=\s*([\d\.]+)', item)
if match:
minv = "%.3s" % match.group(1)
continue
match = re.match(r'<<\s*([\d\.]+)', item)
if match:
maxv = "%.3s" % match.group(1)
continue
match = re.match(r'^[\d\.]+$', item)
if match:
hardcoded.add("%.3s" % match.group(0))
if len(hardcoded) == 1:
ver = hardcoded.pop()
return getver(ver), getver(ver)
if not minv and hardcoded:
# yeah, no maxv!
minv = sorted(hardcoded)[0]
return getver(minv) if minv else None, getver(maxv) if maxv else None
def vrange_str(vrange):
"""Return version range string from given range.
>>> vrange_str(((3, 4), None))
'3.4-'
>>> vrange_str(((3, 4), (3, 6)))
'3.4-3.6'
>>> vrange_str(((3, 4), (4, 0)))
'3.4-4.0'
>>> vrange_str((None, (3, 7)))
'-3.7'
>>> vrange_str(((3, 5), (3, 5)))
'3.5'
>>> vrange_str((None, None))
'-'
"""
if vrange[0] is vrange[1] is None:
return '-'
if vrange[0] == vrange[1]:
return '.'.join(str(i) for i in vrange[0])
elif vrange[0] is None:
return '-' + '.'.join(str(i) for i in vrange[1])
elif vrange[1] is None:
return '.'.join(str(i) for i in vrange[0]) + '-'
else:
return "%s-%s" % ('.'.join(str(i) for i in vrange[0]),
'.'.join(str(i) for i in vrange[1]))
def vrepr(value):
"""
>>> vrepr(([3, 4], [3, 2]))
['3.4', '3.2']
>>> vrepr(('2.6', '3.1'))
['2.6', '3.1']
>>> vrepr('3.2')
'3.2'
>>> vrepr((3, 2))
'3.2'
"""
if isinstance(value, str):
return value
elif not isinstance(value, (GeneratorType, set)) and isinstance(value[0], int):
return '.'.join(str(i) for i in value)
result = []
for version in value:
if isinstance(version, str):
result.append(version)
else:
result.append('.'.join(str(i) for i in version))
return result
def getver(value):
"""Return pair of integers that represent version.
>>> getver('3.2')
(3, 2)
>>> getver('3.1.4')
(3, 1)
>>> getver((3, 3, 1))
(3, 3)
>>> getver(None)
''
"""
if not value:
return ''
if isinstance(value, tuple):
return value[:2]
return tuple(int(i) for i in value.split('.', 2))[:2]
def debsorted(versions, return_str=None):
"""Return sorted list of versions starting with default Python
version (if available) then list of suppored versions greater than default
one followed by reversed list of older versions.
List of versions sorted this way can be used in Depends field.
:param vrepr: return string represenatations of versions, by default the
same format is used as in :param:`versions`
>>> debsorted([(2, 6), (3, 1), (3, 2), (3, 3), (2, 7)])[0] == DEFAULT
True
>>> debsorted(('2.4', '3.2', '2.6', '2.7'))[-1]
(2, 4)
>>> debsorted(set([(2, 1), (2, 2)]))
[(2, 2), (2, 1)]
>>> debsorted([(2, 1), (2, 2)], return_str=True)
['2.2', '2.1']
"""
result = []
old_versions = []
for version in sorted(versions):
if isinstance(version, str):
version = getver(version)
if version < DEFAULT:
old_versions.append(version)
else:
result.append(version)
result.extend(reversed(old_versions))
if return_str and result:
return vrepr(result)
return result
+359
View File
@@ -0,0 +1,359 @@
#! usr/bin/python3
import os
import re
import sys
_defaults = None
_old_versions = None
_unsupported_versions = None
_supported_versions = ["python%s" % ver.strip() for ver in
os.environ.get('DEBPYTHON3_SUPPORTED', '').split(',')
if ver.strip()]
#_default_version = "python%s" % os.environ.get('DEBPYTHON3_DEFAULT', '')
#if _default_version == 'python':
# _default_version = None
_default_version = None
def read_default(name=None):
global _defaults
from configparser import ConfigParser, NoOptionError
if not _defaults:
if os.path.exists('/usr/share/python3/debian_defaults'):
config = ConfigParser()
defaultsfile = open('/usr/share/python3/debian_defaults')
config.read_file(defaultsfile)
defaultsfile.close()
_defaults = config
if _defaults and name:
try:
value = _defaults.get('DEFAULT', name)
except NoOptionError:
raise ValueError
return value
return None
def version_to_tuple(version):
return tuple(int(part) for part in version.split('.'))
def parse_versions(vstring):
if len(vstring.split(',')) > 2:
raise ValueError('too many arguments provided for X-Python3-Version: min and max only.')
import operator
operators = {None: operator.eq, '=': operator.eq,
'>=': operator.ge, '<=': operator.le,
'<<': operator.lt}
vinfo = {}
exact_versions = set()
version_range = set(supported_versions(version_only=True))
relop_seen = False
for field in vstring.split(','):
field = field.strip()
if field == 'all':
continue
if field in ('current', 'current_ext'):
continue
vinfo.setdefault('versions', set())
ve = re.compile(r'(>=|<=|<<|=)? *(\d\.\d+)$')
m = ve.match(field)
try:
if not m:
raise ValueError('error parsing Python3-Version attribute')
op, v = m.group(1), m.group(2)
vmaj, vmin = v.split('.')
if int(vmaj) < 3:
continue
if op in (None, '='):
exact_versions.add(v)
else:
relop_seen = True
filtop = operators[op]
version_range = [av for av in version_range if filtop(
version_to_tuple(av), version_to_tuple(v))]
except Exception:
raise ValueError('error parsing Python3-Version attribute')
if 'versions' in vinfo:
vinfo['versions'] = exact_versions
if relop_seen:
vinfo['versions'] = exact_versions.union(version_range)
return vinfo
def old_versions(version_only=False):
global _old_versions
if not _old_versions:
try:
value = read_default('old-versions')
_old_versions = [s.strip() for s in value.split(',')]
except ValueError:
_old_versions = []
if version_only:
return [v[6:] for v in _old_versions]
else:
return _old_versions
def unsupported_versions(version_only=False):
global _unsupported_versions
if not _unsupported_versions:
try:
value = read_default('unsupported-versions')
_unsupported_versions = [s.strip() for s in value.split(',')]
except ValueError:
_unsupported_versions = []
if version_only:
return [v[6:] for v in _unsupported_versions]
else:
return _unsupported_versions
def supported_versions(version_only=False):
global _supported_versions,_default_version
default_version()
if not _supported_versions:
try:
value = read_default('supported-versions')
_supported_versions = [s.strip() for s in value.split(',')]
except ValueError:
cmd = ['/usr/bin/apt-cache', '--no-all-versions',
'show', 'python3-all']
try:
import subprocess
p = subprocess.Popen(cmd, bufsize=1,
shell=False, stdout=subprocess.PIPE)
fd = p.stdout
except ImportError:
fd = os.popen(' '.join(cmd))
depends = None
for line in fd:
if line.startswith('Depends:'):
depends = line.split(':', 1)[1].strip().split(',')
fd.close()
depends = [re.sub(r'\s*(\S+)[ (]?.*', r'\1', s) for s in depends]
_supported_versions = depends
default = _supported_versions.pop(_supported_versions.index(_default_version))
_supported_versions.sort()
_supported_versions.append(default)
if version_only:
return [v[6:] for v in _supported_versions]
else:
return _supported_versions
def default_version(version_only=False):
global _default_version
if not _default_version:
_default_version = os.readlink('/usr/bin/python3')
# consistency check
debian_default = read_default('default-version')
if not _default_version in (debian_default, os.path.join('/usr/bin', debian_default)):
raise ValueError("the symlink /usr/bin/python3 does not point to the "
"python3 default version. It must be reset "
"to point to %s" % debian_default)
_default_version = debian_default
if version_only:
return _default_version[6:]
else:
return _default_version
def requested_versions(vstring, version_only=False):
global _default_version
default_version()
versions = None
vinfo = parse_versions(vstring)
supported = supported_versions(version_only=True)
if len(vinfo) == 1:
versions = vinfo['versions'].intersection(supported)
vl = []
for version in versions: vl.append(version)
try:
default = vl.pop(vl.index(_default_version[6:]))
except:
default = ''
vl.sort()
if default:
vl.append(default)
else:
raise ValueError('No supported python3 versions in version string')
if not versions:
raise ValueError('empty set of versions')
if version_only:
return vl
else:
return ['python%s' % v for v in vl]
def installed_versions(version_only=False):
import glob
supported = supported_versions()
versions = [os.path.basename(s)
for s in glob.glob('/usr/bin/python3.[0-9]') + glob.glob('/usr/bin/python3.[0-9][0-9]')
if os.path.basename(s) in supported]
versions.sort()
if version_only:
return [v[6:] for v in versions]
else:
return versions
def minmax_supported_version(minmax, version_only=False):
supported_versions_list = supported_versions(True)
version = minmax(version_to_tuple(ver) for ver in supported_versions_list)
version_str = '%d.%d' % version
if version_only:
return version_str
else:
return 'python' + version_str
class ControlFileValueError(ValueError):
pass
class MissingVersionValueError(ValueError):
pass
def extract_pyversion_attribute(fn, pkg):
"""read the debian/control file, extract the X-Python3-Version
field."""
version = None
sversion = None
section = None
with open(fn, encoding='utf-8') as controlfile:
lines = [line.strip() for line in controlfile]
for line in lines:
if line == '' and section != None:
if pkg == 'Source':
break
section = None
elif line.startswith('Source:'):
section = 'Source'
elif line.startswith('Package: ' + pkg):
section = pkg
elif line.lower().startswith('x-python3-version:'):
if section != 'Source':
raise ValueError('attribute X-Python3-Version not in Source section')
sversion = line.split(':', 1)[1].strip()
if section is None:
raise ControlFileValueError('not a control file')
if pkg == 'Source':
if sversion is None:
raise MissingVersionValueError('no X-Python3-Version in control file')
return sversion
return version
'''
def requested_versions_bis(vstring, version_only=False):
versions = []
py_supported_short = supported_versions(version_only=True)
for item in vstring.split(','):
v=item.split('-')
if len(v)>1:
if not v[0]:
v[0] = py_supported_short[0]
if not v[1]:
v[1] = py_supported_short[-1]
for ver in py_supported_short:
try:
if version_cmp(ver,v[0]) >= 0 \
and version_cmp(ver,v[1]) <= 0:
versions.append(ver)
except ValueError:
pass
else:
if v[0] in py_supported_short:
versions.append(v[0])
versions.sort(version_cmp)
if not versions:
raise ValueError('empty set of versions')
if not version_only:
versions=['python'+i for i in versions]
return versions
'''
def main():
from optparse import OptionParser
usage = '[-v] [-h] [-d|--default] [-s|--supported] [-i|--installed] '
'[-r|--requested <version string>|<control file>]'
parser = OptionParser(usage=usage)
parser.add_option('-d', '--default',
help='print the default python3 version',
action='store_true', dest='default')
parser.add_option('-s', '--supported',
help='print the supported python3 versions',
action='store_true', dest='supported')
parser.add_option('-r', '--requested',
help='print the python3 versions requested by a build; '
'the argument is either the name of a control file '
'or the value of the X-Python3-Version attribute',
action='store_true', dest='requested')
parser.add_option('-i', '--installed',
help='print the installed supported python3 versions',
action='store_true', dest='installed')
parser.add_option('--min-supported',
help='print the minimum supported python3 version',
action='store_true', dest='minsupported')
parser.add_option('--max-supported',
help='print the maximum supported python3 version',
action='store_true', dest='maxsupported')
parser.add_option('-v', '--version',
help='print just the version number(s)',
default=False, action='store_true', dest='version_only')
opts, args = parser.parse_args()
program = os.path.basename(sys.argv[0])
if opts.default and len(args) == 0:
try:
print(default_version(opts.version_only))
except ValueError as msg:
print("%s:" % program, msg)
sys.exit(1)
elif opts.supported and len(args) == 0:
print(' '.join(supported_versions(opts.version_only)))
elif opts.installed and len(args) == 0:
print(' '.join(installed_versions(opts.version_only)))
elif opts.minsupported and len(args) == 0:
print(minmax_supported_version(min, opts.version_only))
elif opts.maxsupported and len(args) == 0:
print(minmax_supported_version(max, opts.version_only))
elif opts.requested and len(args) <= 1:
if len(args) == 0:
versions = 'debian/control'
else:
versions = args[0]
try:
if os.path.isfile(versions):
fn = versions
try:
vstring = extract_pyversion_attribute(fn, 'Source')
vs = requested_versions(vstring, opts.version_only)
except ControlFileValueError:
sys.stderr.write("%s: not a control file: %s, "
% (program, fn))
sys.exit(1)
except MissingVersionValueError:
sys.stderr.write("%s: no X-Python3-Version in control "
"file, using supported versions\n" %
program)
vs = supported_versions(opts.version_only)
else:
vs = requested_versions(versions, opts.version_only)
print(' '.join(vs))
except ValueError as msg:
sys.stderr.write("%s: %s\n" % (program, msg))
sys.exit(1)
else:
sys.stderr.write("usage: %s %s\n" % (program, usage))
sys.exit(1)
if __name__ == '__main__':
main()
+46
View File
@@ -0,0 +1,46 @@
# some macros useful for packaging python packages
# to include it unconditionally:
# include /usr/share/python3/python.mk
#
# to include it conditionally, and have the packaging working with earlier releases
# and backports:
# -include /usr/share/python/python.mk
# ifeq (,$(py_sitename))
# py_sitename = site-packages
# py_libdir = /usr/lib/python$(subst python,,$(1))/site-packages
# py_sitename_sh = $(py_sitename)
# py_libdir_sh = $(py_libdir)
# endif
# py_sitename: name of the site-packages/dist-packages directory depending
# on the python version. Call as: $(call py_sitename, <python version>).
# Don't use this in shell snippets inside loops.
py_sitename = $(if $(filter $(subst python,,$(1)), 2.3 2.4 2.5),site,dist)-packages
# py_libdir: absolute path to the default python library for third party
# stuff. Call as: $(call py_libdir, <python version>).
# Don't use this in shell snippets inside loops.
py_libdir = /usr/lib/python$(strip $(if $(findstring 3.,$(subst python,,$(1))),3,$(subst python,,$(1))))/$(py_sitename)
# py_pkgname: package name corresponding to the python version.
# Call as: $(call py_pkgname, <path>, <python version>).
py_pkgname = $(if $(findstring 3.,$(2)),$(subst python-,python3-,$(1)),$(1))
# setuptools' build directory
py_builddir = $(shell python$(strip $(subst python,,$(1))) -c 'import setuptools; from distutils.command.build import build; from distutils.core import Distribution; b = build(Distribution()); b.finalize_options(); print(b.build_platlib)')
# The same macros for use inside loops in shell snippets
py_sitename_sh = $$(basename $$(_py_=$(strip $(1)); python$${_py_\#python*} -c 'import setuptools; from distutils import sysconfig; print(sysconfig.get_python_lib())'))
py_libdir_sh = $$(_py_=$(strip $(1)); python$${_py_\#python*} -c 'import setuptools; from distutils import sysconfig; print(sysconfig.get_python_lib())')
py_builddir_sh = $$(_py_=$(strip $(1)); python$${_py_\#python*} -c 'import setuptools; from distutils.command.build import build; from distutils.core import Distribution; b = build(Distribution()); b.finalize_options(); print(b.build_platlib)')
# Arguments to pass to setup.py install
py_setup_install_args = --install-layout=deb
@@ -0,0 +1,10 @@
#! /bin/sh
set -e
VERSION=${2#python}
if which py3compile >/dev/null 2>&1; then
py3compile -V $VERSION /usr/lib/python3/dist-packages
else
echo >&2 "py3compile not found in $(basename $0) hook."
exit 1
fi
@@ -0,0 +1,20 @@
#! /bin/sh
set -e
VERSION=${2#python}
if which py3clean >/dev/null 2>&1; then
if [ "$VERSION" = 3.1 ]; then
find /usr/lib/python3.1/dist-packages -name '*.py[co]' -delete
find /usr/lib/python3/dist-packages -name '*.py[co]' -delete
else
py3clean -V $VERSION /usr/lib/python3/dist-packages
fi
else
if [ "$VERSION" = 3.1 ]; then
find /usr/lib/python3.1/dist-packages -name '*.py[co]' -delete
else
TAG=`python$VERSION -c "import sys; print(sys.implementation.cache_tag)"` \
find /usr/lib/python3/dist-packages -name "*.$TAG.py[co]" -delete
find /usr/lib/python3/dist-packages -depth -empty -name '__pycache__' -delete
fi
fi