diff options
Diffstat (limited to '')
225 files changed, 15090 insertions, 0 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..842c1ce --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,14 @@ +default: + image: debian:unstable + +tests: + before_script: + - apt-get update + - apt-get -y install --no-install-recommends build-essential debhelper fakeroot flit libjs-jquery python3-all python3-all-dbg python3-all-dev python3-build python3-installer python3-nose2 python3-poetry-core python3-pytest python3-setuptools python3-tomli tox + + script: + - make tests + - echo -e '#!/bin/sh\nset -eu\nmake "$@"' > debian/tests/run-installed + - export DH_PYTHON_DIST=$PWD/pydist + - ./debian/tests/dh-python + - ./debian/tests/pybuild diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f660604 --- /dev/null +++ b/Makefile @@ -0,0 +1,62 @@ +#!/usr/bin/make -f +INSTALL ?= install +PREFIX ?= /usr/local +MANPAGES ?= pybuild.1 pybuild-autopkgtest.1 dh_python3.1 +DVERSION=$(shell dpkg-parsechangelog | sed -rne 's,^Version: (.+),\1,p' || echo 'DEVEL') +VERSION=$(shell dpkg-parsechangelog | sed -rne 's,^Version: ([^-]+).*,\1,p' || echo 'DEVEL') + +clean: + make -C tests clean + make -C pydist clean + find . -name '*.py[co]' -delete + find . -name __pycache__ -type d | xargs rm -rf + rm -f .coverage $(MANPAGES) + rm -rf .pybuild + +dist: + git archive --format=tar --prefix=dh-python-$(VERSION)/ HEAD \ + | xz -9 -c >../dh-python_$(VERSION).orig.tar.xz + +install: + $(INSTALL) -m 755 -d $(DESTDIR)$(PREFIX)/bin \ + $(DESTDIR)$(PREFIX)/share/debhelper/autoscripts/ \ + $(DESTDIR)$(PREFIX)/share/perl5/Debian/Debhelper/Sequence/ \ + $(DESTDIR)$(PREFIX)/share/perl5/Debian/Debhelper/Buildsystem/ \ + $(DESTDIR)$(PREFIX)/share/dh-python/dhpython/build \ + $(DESTDIR)$(PREFIX)/share/dh-python/dist + $(INSTALL) -m 644 pydist/*_fallback $(DESTDIR)$(PREFIX)/share/dh-python/dist/ + $(INSTALL) -m 644 dhpython/*.py $(DESTDIR)$(PREFIX)/share/dh-python/dhpython/ + $(INSTALL) -m 644 dhpython/build/*.py $(DESTDIR)$(PREFIX)/share/dh-python/dhpython/build/ + $(INSTALL) -m 755 pybuild $(DESTDIR)$(PREFIX)/share/dh-python/ + $(INSTALL) -m 755 pybuild-autopkgtest $(DESTDIR)$(PREFIX)/share/dh-python/ + $(INSTALL) -m 755 dh_python3 $(DESTDIR)$(PREFIX)/share/dh-python/ + sed -i -e 's/DEVELV/$(DVERSION)/' $(DESTDIR)$(PREFIX)/share/dh-python/pybuild + sed -i -e 's/DEVELV/$(DVERSION)/' $(DESTDIR)$(PREFIX)/share/dh-python/dh_python3 + + $(INSTALL) -m 644 dh/pybuild.pm $(DESTDIR)$(PREFIX)/share/perl5/Debian/Debhelper/Buildsystem/ + $(INSTALL) -m 644 dh/python3.pm $(DESTDIR)$(PREFIX)/share/perl5/Debian/Debhelper/Sequence/ + $(INSTALL) -m 644 autoscripts/* $(DESTDIR)$(PREFIX)/share/debhelper/autoscripts/ + +%.1: %.rst + rst2man $< > $@ + +%.htm: %.rst + rst2html $< > $@ + +manpages: $(MANPAGES) + +dist_fallback: + make -C pydist $@ + +# TESTS +nose: + #nosetests3 --verbose --with-doctest --with-coverage + nose2-3 --verbose --plugin nose2.plugins.doctests --with-doctest + +tests: nose + make -C tests + +test%: + make -C tests $@ + +.PHONY: clean tests test% check_versions diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..0195c1b --- /dev/null +++ b/README.rst @@ -0,0 +1,175 @@ +=========== + dh-python +=========== + +``dh-python`` provides various tools that help packaging Python related files +in Debian. + +* ``pybuild`` is a tool that implements ``dh`` sequencer's ``dh_auto_foo`` + commands (but it can be used outside ``dh`` as well). It builds and installs + files. + +* ``dh_python3`` is a tool that takes what ``pybuild`` produces and + generates runtime dependencies and maintainer scripts. + It fixes some common mistakes, like installing files into + ``site-packages`` instead of ``dist-packages``, ``/usr/local/bin/`` + shebangs, removes ``.py`` files from ``-dbg`` packages, etc.) + + To translate ``requires.txt`` (a file installed in + ``dist-packages/foo.egg-info/``) into Debian dependencies, a list of + packages that provide given egg distribution is used. If the dependency + is not found there, ``dpkg -S`` is used (i.e. a given dependency has to be + installed; you need it in ``Build-Depends`` in order to run tests anyway). + See *dependencies* section in ``dh_python3``'s manpage for more details. + + * ``dh_python3`` works on ``./debian/python3-foo/`` files and other binary + packages that have ``${python3:Depends}`` in the ``Depends`` field. + It ignores Python 2.X and PyPy2 specific directories. + See ``dh_python3`` manpage for more details. + +How it works +============ + +A simplified work flow looks like this: + +.. code:: python + + # dh_auto_clean stage + for interpreter in REQUESTED_INTERPRETERS: + for version in interpreter.REQUESTED_VERSIONS: + PYBUILD_BEFORE_CLEAN + pybuild --clean + PYBUILD_AFTER_CLEAN + + plenty_of_other_dh_foo_tools_invoked_here + + # dh_auto_configure stage + for interpreter in REQUESTED_INTERPRETERS: + for version in interpreter.REQUESTED_VERSIONS: + PYBUILD_BEFORE_CONFIGURE + pybuild --configure + PYBUILD_AFTER_CONFIGURE + + plenty_of_other_dh_foo_tools_invoked_here + + # dh_auto_build stage + for interpreter in REQUESTED_INTERPRETERS: + for version in interpreter.REQUESTED_VERSIONS: + PYBUILD_BEFORE_BUILD + pybuild --build + PYBUILD_AFTER_BUILD + + plenty_of_other_dh_foo_tools_invoked_here + + # dh_auto_test stage + for interpreter in REQUESTED_INTERPRETERS: + for version in interpreter.REQUESTED_VERSIONS: + PYBUILD_BEFORE_TEST + pybuild --test + PYBUILD_AFTER_TEST + + plenty_of_other_dh_foo_tools_invoked_here + + # dh_auto_install stage + for interpreter in REQUESTED_INTERPRETERS: + for version in interpreter.REQUESTED_VERSIONS: + PYBUILD_BEFORE_INSTALL + pybuild --install + PYBUILD_AFTER_INSTALL + + plenty_of_other_dh_foo_tools_invoked_here + + dh_python3 + + plenty_of_other_dh_foo_tools_invoked_here + + +pybuild --$step +--------------- + +This command is auto-detected, it currently supports distutils, autotools, +cmake and a custom build system where you can define your own set of +commands. Why do we need it? ``dh_auto_foo`` doesn't know each command has to +be invoked for each interpreter and version. + + +REQUESTED_INTERPRETERS +---------------------- + +is parsed from ``Build-Depends`` if ``--buildsystem=pybuild`` is set. If it's +not, you have to pass ``--interpreter`` to ``pybuild`` (more in its manpage) + +* ``python3-all{,-dev}`` - all CPython interpreters (for packages that + provide public modules / extensions) +* ``python3-all-dbg`` - all CPython debug interpreters (if ``-dbg`` package + is provided) +* ``python3`` - default CPython or closest to default interpreter only (use + this if you build a Python application) +* ``python3-dbg`` - default CPython debug (or closest to the default one) + only + +REQUESTED_VERSIONS +------------------ + +is parsed from ``X-Python3-Version`` and ``Build-Depends``. + + +BEFORE and AFTER commands +------------------------- + +can be different for each interpreter and/or version, examples: + +* ``PYBUILD_AFTER_BUILD_python3.5=rm {destdir}/{build_dir}/foo/bar2X.py`` +* ``PYBUILD_BEFORE_INSTALL_python3=touch {destdir}/{install_dir}/foo/bar/__init__.py`` + +These commands should be used only if overriding ``dh_auto_foo`` is not enough +(example below) + +.. code:: + + override_dh_auto_install: + before_auto_install_commands + dh_auto_install + after_auto_install_commands + +See the ``pybuild`` manpage for more details (search for ``BUILD SYSTEM ARGUMENTS``) + + +overrides +--------- + +How to override ``pybuild`` autodetected options: +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +* Each ``pybuild`` call can be disabled (for given interpreter, version or + stage). See the ``pybuild`` manpage for more details (search for + ``--disable`` description). +* You can pass options in ``override_dh_auto_foo`` via command line options: + + .. code:: + + dh_auto_test -- --system=custom --test-args='{interpreter} setup.py test' + + or env. variables: + + .. code:: + + PYBUILD_SYSTEM=custom PYBUILD_TEST_ARGS='{interpreter} setup.py test' dh_auto_test + +* You can export env. variables globally at the beginning of debian/rules + + .. code:: + + export PYBUILD_TEST_ARGS={dir}/tests/ + +How to override dh_python3 options: +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + * via command line, f.e. + +.. code:: + + override_dh_python3: + dh_python3 --shebang=/usr/bin/python3 + diff --git a/autoscripts/postinst-py3compile b/autoscripts/postinst-py3compile new file mode 100644 index 0000000..2af81b0 --- /dev/null +++ b/autoscripts/postinst-py3compile @@ -0,0 +1,6 @@ +if command -v py3compile >/dev/null 2>&1; then + py3compile -p #PACKAGE# #ARGS# +fi +if command -v pypy3compile >/dev/null 2>&1; then + pypy3compile -p #PACKAGE# #ARGS# || true +fi diff --git a/autoscripts/postinst-pycompile b/autoscripts/postinst-pycompile new file mode 100644 index 0000000..f479f84 --- /dev/null +++ b/autoscripts/postinst-pycompile @@ -0,0 +1,3 @@ +if command -v pycompile >/dev/null 2>&1; then + pycompile -p #PACKAGE# #ARGS# +fi diff --git a/autoscripts/postinst-pypycompile b/autoscripts/postinst-pypycompile new file mode 100644 index 0000000..b26c45f --- /dev/null +++ b/autoscripts/postinst-pypycompile @@ -0,0 +1,5 @@ +if command -v pypycompile >/dev/null 2>&1; then + pypycompile -p #PACKAGE# #ARGS# +elif pypy -m py_compile >/dev/null 2>&1; then + dpkg -L #PACKAGE# | grep '\.py$' | pypy -m py_compile - >/dev/null +fi diff --git a/autoscripts/prerm-py3clean b/autoscripts/prerm-py3clean new file mode 100644 index 0000000..3ec270f --- /dev/null +++ b/autoscripts/prerm-py3clean @@ -0,0 +1,6 @@ +if command -v py3clean >/dev/null 2>&1; then + py3clean -p #PACKAGE# #ARGS# +else + dpkg -L #PACKAGE# | sed -En -e '/^(.*)\/(.+)\.py$/s,,rm "\1/__pycache__/\2".*,e' + find /usr/lib/python3/dist-packages/ -type d -name __pycache__ -empty -print0 | xargs --null --no-run-if-empty rmdir +fi diff --git a/autoscripts/prerm-pyclean b/autoscripts/prerm-pyclean new file mode 100644 index 0000000..a208c9b --- /dev/null +++ b/autoscripts/prerm-pyclean @@ -0,0 +1,8 @@ +if command -v pyclean >/dev/null 2>&1; then + pyclean -p #PACKAGE# #ARGS# +else + dpkg -L #PACKAGE# | grep \.py$ | while read file + do + rm -f "${file}"[co] >/dev/null + done +fi diff --git a/autoscripts/prerm-pypyclean b/autoscripts/prerm-pypyclean new file mode 100644 index 0000000..8e7b2bc --- /dev/null +++ b/autoscripts/prerm-pypyclean @@ -0,0 +1,6 @@ +if command -v pypyclean >/dev/null 2>&1; then + pypyclean -p #PACKAGE# #ARGS# +else + dpkg -L #PACKAGE# | sed -En -e '/^(.*)\/(.+)\.py$/s,,rm "\1/__pycache__/\2".*,e' + find /usr/lib/pypy/dist-packages/ -type d -name __pycache__ -empty -print0 | xargs --null --no-run-if-empty rmdir +fi diff --git a/dh/pybuild.pm b/dh/pybuild.pm new file mode 100644 index 0000000..31b52a1 --- /dev/null +++ b/dh/pybuild.pm @@ -0,0 +1,226 @@ +# A debhelper build system class for building Python libraries +# +# Copyright: © 2012-2013 Piotr Ożarowski + +# TODO: +# * support for dh --parallel + +package Debian::Debhelper::Buildsystem::pybuild; + +use strict; +use Dpkg::Control; +use Dpkg::Changelog::Debian; +use Debian::Debhelper::Dh_Lib qw(%dh error doit); +use base 'Debian::Debhelper::Buildsystem'; + +sub DESCRIPTION { + "Python pybuild" +} + +sub check_auto_buildable { + my $this=shift; + return doit('pybuild', '--detect', '--really-quiet', '--dir', $this->get_sourcedir()); +} + +sub new { + my $class=shift; + my $this=$class->SUPER::new(@_); + $this->enforce_in_source_building(); + + if (!$ENV{'PYBUILD_INTERPRETERS'}) { + if ($ENV{'DEBPYTHON_DEFAULT'}) { + $this->{pydef} = $ENV{'DEBPYTHON_DEFAULT'};} + else { + $this->{pydef} = `pyversions -vd 2>/dev/null`;} + $this->{pydef} =~ s/\s+$//; + if ($ENV{'DEBPYTHON_SUPPORTED'}) { + $this->{pyvers} = $ENV{'DEBPYTHON_SUPPORTED'} =~ s/,/ /r;} + else { + $this->{pyvers} = `pyversions -vr 2>/dev/null`;} + $this->{pyvers} =~ s/\s+$//; + if ($ENV{'DEBPYTHON3_DEFAULT'}) { + $this->{py3def} = $ENV{'DEBPYTHON3_DEFAULT'};} + else { + $this->{py3def} = `py3versions -vd 2>/dev/null`;} + $this->{py3def} =~ s/\s+$//; + if ($ENV{'DEBPYTHON3_SUPPORTED'}) { + $this->{py3vers} = $ENV{'DEBPYTHON3_SUPPORTED'} =~ s/,/ /r;} + else { + $this->{py3vers} = `py3versions -vr 2>/dev/null`; + if ($this->{py3vers} eq "") { + # We swallowed stderr, above + system("py3versions -vr"); + die('E: py3versions failed'); + } + } + $this->{py3vers} =~ s/\s+$//; + } + + return $this; +} + +sub configure { + my $this=shift; + foreach my $command ($this->pybuild_commands('configure', @_)) { + doit(@$command); + } +} + +sub build { + my $this=shift; + foreach my $command ($this->pybuild_commands('build', @_)) { + doit(@$command); + } +} + +sub install { + my $this=shift; + my $destdir=shift; + foreach my $command ($this->pybuild_commands('install', @_)) { + doit(@$command, '--dest-dir', $destdir); + } +} + +sub test { + my $this=shift; + foreach my $command ($this->pybuild_commands('test', @_)) { + doit(@$command); + } +} + +sub clean { + my $this=shift; + foreach my $command ($this->pybuild_commands('clean', @_)) { + doit(@$command); + } + doit('rm', '-rf', '.pybuild/'); + doit('find', '.', '-name', '*.pyc', '-exec', 'rm', '{}', ';'); +} + +sub pybuild_commands { + my $this=shift; + my $step=shift; + my @options = @_; + my @result; + + my $dir = $this->get_sourcedir(); + if (not grep {$_ eq '--dir'} @options and $dir ne '.') { + # if --dir is not passed, PYBUILD_DIR can be used + push @options, '--dir', $dir; + } + + if (not grep {$_ eq '--verbose'} @options and $dh{QUIET}) { + push @options, '--quiet'; + } + + my @deps; + if ($ENV{'PYBUILD_INTERPRETERS'}) { + push @result, ['pybuild', "--$step", @options]; + } + else { + # get interpreter packages from Build-Depends{,-Indep}: + # NOTE: possible problems with alternative/versioned dependencies + @deps = $this->python_build_dependencies(); + + # When depends on python{3,}-setuptools-scm, set + # SETUPTOOLS_SCM_PRETEND_VERSION to upstream version + # Without this, setuptools-scm tries to detect current + # version from git tag, which fails for debian tags + # (debian/<version>) sometimes. + if ((grep /python3-(setuptools-scm|hatch-vcs)/, @deps) && !$ENV{'SETUPTOOLS_SCM_PRETEND_VERSION'}) { + my $changelog = Dpkg::Changelog::Debian->new(range => {"count" => 1}); + $changelog->load("debian/changelog"); + my $version = @{$changelog}[0]->get_version(); + $version =~ s/-[^-]+$//; # revision + $version =~ s/^\d+://; # epoch + $version =~ s/~/-/; # ignore tilde versions + $ENV{'SETUPTOOLS_SCM_PRETEND_VERSION'} = $version; + } + + # When depends on python{3,}-pbr, set PBR_VERSION to upstream version + # Without this, python-pbr tries to detect current + # version from pkg metadata or git tag, which fails for debian tags + # (debian/<version>) sometimes. + if ((grep /python3-pbr/, @deps) && !$ENV{'PBR_VERSION'}) { + my $changelog = Dpkg::Changelog::Debian->new(range => {"count" => 1}); + $changelog->load("debian/changelog"); + my $version = @{$changelog}[0]->get_version(); + $version =~ s/-[^-]+$//; # revision + $version =~ s/^\d+://; # epoch + $ENV{'PBR_VERSION'} = $version; + } + + my @py3opts = ('pybuild', "--$step"); + + if (($step eq 'test' or $step eq 'autopkgtest') and + $ENV{'PYBUILD_TEST_PYTEST'} ne '1' and + $ENV{'PYBUILD_TEST_NOSE2'} ne '1' and + $ENV{'PYBUILD_TEST_NOSE'} ne '1' and + $ENV{'PYBUILD_TEST_CUSTOM'} ne '1' and + $ENV{'PYBUILD_TEST_TOX'} ne '1') { + if (grep {$_ eq 'tox'} @deps and $ENV{'PYBUILD_TEST_TOX'} ne '0') { + push @py3opts, '--test-tox'} + elsif (grep {$_ eq 'python3-pytest'} @deps and $ENV{'PYBUILD_TEST_PYTEST'} ne '0') { + push @py3opts, '--test-pytest'} + elsif (grep {$_ eq 'python3-nose2'} @deps and $ENV{'PYBUILD_TEST_NOSE2'} ne '0') { + push @py3opts, '--test-nose2'} + elsif (grep {$_ eq 'python3-nose'} @deps and $ENV{'PYBUILD_TEST_NOSE'} ne '0') { + push @py3opts, '--test-nose'} + } + + my $py3all = 0; + my $py3alldbg = 0; + + my $i = 'python{version}'; + + # Python 3 + if ($this->{py3vers}) { + if (grep {$_ eq 'python3-all' or $_ eq 'python3-all-dev'} @deps) { + $py3all = 1; + push @result, [@py3opts, '-i', $i, '-p', $this->{py3vers}, @options]; + } + if (grep {$_ eq 'python3-all-dbg'} @deps) { + $py3alldbg = 1; + push @result, [@py3opts, '-i', "$i-dbg", '-p', $this->{py3vers}, @options]; + } + } + if ($this->{py3def}) { + if (not $py3all and grep {$_ eq 'python3' or $_ eq 'python3-dev'} @deps) { + push @result, [@py3opts, '-i', $i, '-p', $this->{py3def}, @options]; + } + if (not $py3alldbg and grep {$_ eq 'python3-dbg'} @deps) { + push @result, [@py3opts, '-i', "$i-dbg", '-p', $this->{py3def}, @options]; + } + } + # TODO: pythonX.Y → `pybuild -i python{version} -p X.Y` + + } + if (!@result) { + use Data::Dumper; + die('E: Please add appropriate interpreter package to Build-Depends, see pybuild(1) for details.' . + 'this: ' . Dumper($this) . + 'deps: ' . Dumper(\@deps)); + } + return @result; +} + +sub python_build_dependencies { + my $this=shift; + + my @result; + my $c = Dpkg::Control->new(type => CTRL_INFO_SRC); + if ($c->load('debian/control')) { + for my $field (grep /^Build-Depends/, keys %{$c}) { + my $builddeps = $c->{$field}; + while ($builddeps =~ /(?:^|[\s,])((pypy|python|tox)[0-9\.]*(-[^\s,\(]+)?)(?:[\s,\(]|$)/g) { + my $dep = $1; + $dep =~ s/:(any|native)$//; + if ($dep) {push @result, $dep}; + } + } + } + + return @result; +} + +1 diff --git a/dh/python3.pm b/dh/python3.pm new file mode 100644 index 0000000..103ee2d --- /dev/null +++ b/dh/python3.pm @@ -0,0 +1,10 @@ +#! /usr/bin/perl +# debhelper sequence file for dh_python3 + +use warnings; +use strict; +use Debian::Debhelper::Dh_Lib; + +insert_before("dh_installinit", "dh_python3"); + +1 diff --git a/dh_python3 b/dh_python3 new file mode 100755 index 0000000..561ade7 --- /dev/null +++ b/dh_python3 @@ -0,0 +1,292 @@ +#! /usr/bin/python3 +# vim: et ts=4 sw=4 + +# Copyright © 2010-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 sys +from argparse import ArgumentParser, SUPPRESS +from os.path import exists, join +from shutil import copy as fcopy +from dhpython.debhelper import DebHelper +from dhpython.depends import Dependencies +from dhpython.interpreter import Interpreter, EXTFILE_RE +from dhpython.version import supported, default, Version, VersionRange +from dhpython.pydist import validate as validate_pydist +from dhpython.fs import fix_locations, Scan +from dhpython.option import compiled_regex +from dhpython.tools import pyinstall, pyremove + +# initialize script +logging.basicConfig(format='%(levelname).1s: dh_python3 ' + '%(module)s:%(lineno)d: %(message)s') +log = logging.getLogger('dhpython') +os.umask(0o22) +DEFAULT = default('cpython3') +SUPPORTED = supported('cpython3') + + +class Scanner(Scan): + def handle_ext(self, fpath): + path, fname = fpath.rsplit('/', 1) + tagver = EXTFILE_RE.search(fname) + if tagver is None: + # yeah, python3.1 is not covered, but we don't want to + # mess with non-Python libraries, don't we? + return + tagver = tagver.groupdict()['ver'] + if tagver is None: + return + tagver = Version("%s.%s" % (tagver[0], tagver[1:])) + return tagver + + +def main(): + parser = ArgumentParser() + parser.add_argument( + '--version', action='version', version='%(prog)s DEVELV') + parser.add_argument( + '--no-guessing-deps', action='store_false', dest='guess_deps', + help='disable guessing dependencies') + parser.add_argument( + '--skip-private', action='store_true', + help="don't check private directories") + parser.add_argument( + '-v', '--verbose', action='store_true', + default=os.environ.get('DH_VERBOSE') == '1', + help='turn verbose mode on') + # arch=False->arch:all only, arch=True->arch:any only, None->all of them + parser.add_argument( + '-i', '--indep', action='store_false', dest='arch', default=None, + help='act on architecture independent packages') + parser.add_argument( + '-a', '-s', '--arch', action='store_true', dest='arch', + help='act on architecture dependent packages') + parser.add_argument( + '-q', '--quiet', action='store_false', dest='verbose', help='be quiet') + parser.add_argument( + '-p', '--package', action='append', metavar='PACKAGE', + help='act on the package named PACKAGE') + parser.add_argument( + '-N', '--no-package', action='append', metavar='PACKAGE', + help='do not act on the specified package') + parser.add_argument( + '--remaining-packages', action='store_true', + help='Do not act on the packages which have already been acted on by ' + 'this debhelper command earlier') + parser.add_argument( + '--compile-all', action='store_true', + help='compile all files from given private directory in postinst, not ' + 'just the ones provided by the package') + parser.add_argument( + '-V', type=VersionRange, dest='vrange', metavar='[X.Y][-][A.B]', + help='specify list of supported Python versions. See py3compile(1) for ' + 'examples') + parser.add_argument( + '-X', '--exclude', action='append', dest='regexpr', type=compiled_regex, + metavar='REGEXPR', + help='exclude items that match given REGEXPR. You may use this option ' + 'multiple times to build up a list of things to exclude.') + parser.add_argument( + '--accept-upstream-versions', action='store_true', + help='accept upstream versions while translating Python dependencies ' + 'into Debian ones') + parser.add_argument( + '--depends', action='append', metavar='REQ', + help='translate given requirements into Debian dependencies and add ' + 'them to ${python3:Depends}. Use it for missing items in ' + 'requires.txt.') + parser.add_argument( + '--depends-section', action='append', metavar='SECTION', + help='translate requirements from given section into Debian ' + 'dependencies and add them to ${python3:Depends}') + parser.add_argument( + '--recommends', action='append', metavar='REQ', + help='translate given requirements into Debian dependencies and add ' + 'them to ${python3:Recommends}') + parser.add_argument( + '--recommends-section', action='append', metavar='SECTION', + help='translate requirements from given section into Debian ' + 'dependencies and add them to ${python3:Recommends}') + parser.add_argument( + '--suggests', action='append', metavar='REQ', + help='translate given requirements into Debian dependencies and add ' + 'them to ${python3:Suggests}') + parser.add_argument( + '--suggests-section', action='append', metavar='SECTION', + help='translate requirements from given section into Debian ' + 'dependencies and add them to ${python3:Suggests}') + parser.add_argument( + '--requires', action='append', metavar='FILE', + help='translate requirements from given file into Debian dependencies ' + 'and add them to ${python3:Depends}') + parser.add_argument( + '--shebang', metavar='COMMAND', + help='use given command as shebang in scripts') + parser.add_argument( + '--ignore-shebangs', action='store_true', + help='do not translate shebangs into Debian dependencies') + parser.add_argument( + '--no-dbg-cleaning', action='store_false', dest='clean_dbg_pkg', + help='do not remove files from debug packages') + parser.add_argument( + '--no-ext-rename', action='store_true', + help='do not add magic tags nor multiarch tuples to extension file ' + 'names)') + parser.add_argument( + '--no-shebang-rewrite', action='store_true', + help='do not rewrite shebangs') + parser.add_argument('private_dir', nargs='?', + help='Private directory containing Python modules (optional)') + # debhelper options: + parser.add_argument('-O', action='append', help=SUPPRESS) + + options = parser.parse_args(os.environ.get('DH_OPTIONS', '').split() + + sys.argv[1:]) + if options.O: + parser.parse_known_args(options.O, options) + + private_dir = options.private_dir + if private_dir: + if not private_dir.startswith('/'): + # handle usr/share/foo dirs (without leading slash) + private_dir = '/' + private_dir + # TODO: support more than one private dir at the same time (see :meth:scan) + if options.skip_private: + private_dir = False + + if options.verbose: + log.setLevel(logging.DEBUG) + log.debug('version: DEVELV') + log.debug('argv: %s', sys.argv) + log.debug('options: %s', options) + log.debug('supported Python versions: %s (default=%s)', + ','.join(str(v) for v in SUPPORTED), DEFAULT) + else: + log.setLevel(logging.INFO) + + options.write_log = False + if os.environ.get('DH_INTERNAL_OVERRIDE', ''): + options.write_log = True + + try: + dh = DebHelper(options, impl='cpython3') + except Exception as e: + log.error('cannot initialize DebHelper: %s', e) + exit(2) + if not dh.packages: + log.error('no package to act on (python3-foo or one with ${python3:Depends} in Depends)') + # exit(7) + if not options.vrange and dh.python_version: + options.vrange = VersionRange(dh.python_version) + + interpreter = Interpreter('python3') + for package, pdetails in dh.packages.items(): + log.debug('processing package %s...', package) + interpreter.debug = package.endswith('-dbg') + + if not private_dir: + try: + pyinstall(interpreter, package, options.vrange) + except Exception as err: + log.error("%s.pyinstall: %s", package, err) + exit(4) + try: + pyremove(interpreter, package, options.vrange) + except Exception as err: + log.error("%s.pyremove: %s", package, err) + exit(5) + fix_locations(package, interpreter, SUPPORTED, options) + stats = Scanner(interpreter, package, private_dir, options).result + + dependencies = Dependencies(package, 'cpython3', dh.build_depends) + dependencies.parse(stats, options) + + pyclean_added = False # invoke pyclean only once in maintainer script + if stats['compile']: + args = '' + if options.vrange: + args += "-V %s" % options.vrange + dh.autoscript(package, 'postinst', 'postinst-py3compile', args) + dh.autoscript(package, 'prerm', 'prerm-py3clean', '') + pyclean_added = True + for pdir, details in sorted(stats['private_dirs'].items()): + if not details.get('compile'): + continue + if not pyclean_added: + dh.autoscript(package, 'prerm', 'prerm-py3clean', '') + pyclean_added = True + + args = pdir + + ext_for = details.get('ext_vers') + ext_no_version = details.get('ext_no_version') + if ext_for is None and not ext_no_version: # no extension + shebang_versions = list(i.version for i in details.get('shebangs', []) + if i.version and i.version.minor) + if not options.ignore_shebangs and len(shebang_versions) == 1: + # only one version from shebang + args += " -V %s" % shebang_versions[0] + elif options.vrange and options.vrange != (None, None): + args += " -V %s" % options.vrange + elif ext_no_version: + # at least one extension's version not detected + if options.vrange and '-' not in str(options.vrange): + ver = str(options.vrange) + else: # try shebang or default Python version + ver = (list(i.version for i in details.get('shebangs', []) + if i.version and i.version.minor) or [None])[0] or DEFAULT + dependencies.depend("python%s" % ver) + args += " -V %s" % ver + else: + extensions = sorted(ext_for) + vr = VersionRange(minver=extensions[0], maxver=extensions[-1]) + args += " -V %s" % vr + + for regex in options.regexpr or []: + args += " -X '%s'" % regex.pattern.replace("'", r"'\''") + + dh.autoscript(package, 'postinst', 'postinst-py3compile', args) + + dependencies.export_to(dh) + + pydist_file = join('debian', "%s.pydist" % package) + if exists(pydist_file): + if not validate_pydist(pydist_file): + log.warning("%s.pydist file is invalid", package) + else: + dstdir = join('debian', package, 'usr/share/python3/dist/') + if not exists(dstdir): + os.makedirs(dstdir) + fcopy(pydist_file, join(dstdir, package)) + bcep_file = join('debian', "%s.bcep" % package) + if exists(bcep_file): + dstdir = join('debian', package, 'usr/share/python3/bcep/') + if not exists(dstdir): + os.makedirs(dstdir) + fcopy(bcep_file, join(dstdir, package)) + + dh.save() + + +if __name__ == '__main__': + main() diff --git a/dh_python3.rst b/dh_python3.rst new file mode 100644 index 0000000..1c69634 --- /dev/null +++ b/dh_python3.rst @@ -0,0 +1,238 @@ +============ + dh_python3 +============ + +----------------------------------------------------------------------------------- +calculates Python dependencies, adds maintainer scripts to byte compile files, etc. +----------------------------------------------------------------------------------- + +:Manual section: 1 +:Author: Piotr Ożarowski, 2012-2013 + +SYNOPSIS +======== + dh_python3 -p PACKAGE [-V [X.Y][-][A.B]] DIR [-X REGEXPR] + +DESCRIPTION +=========== + +QUICK GUIDE FOR MAINTAINERS +--------------------------- + + * build depend on dh-python + * build-depend on python3 (Python application) or python3-all (Python module) + or python3-all-dev (Python extension), + * if necessary, describe supported Python 3 versions via X-Python3-Version field + in debian/control, + * build module/application using its standard build system (pybuild wrapper + recommended, see pybuild.1 for more details), remember to build extensions + for all supported Python 3 versions (loop over ``py3versions -vr``), + * install files to the *standard* locations, add `--install-layout=deb` to + setup.py's install command if your package is using distutils, + * add `python3` to dh's --with option, or: + * `include /usr/share/cdbs/1/class/python-distutils.mk` in debian/rules and + depend on `cdbs (>= 0.4.90)`, or: + * call ``dh_python3`` in the `binary-*` target, + * add `${python3:Depends}` to Depends + +NOTES +----- + +dependencies +~~~~~~~~~~~~ +dh_python3 tries to translate Python dependencies from `Requires-Dist` +entries in `dist-info` or `requires.txt` contents in `egg-info` to +Debian dependencies. +In many cases, this works without any additional configuration because +dh_python3 comes with a build-in mapping of Python module names to +Debian packages that is periodically regenerated from the Debian +archive. By default, the version information in the Python dependencies is +discarded. If you want dh_python3 to generate more strict dependencies (e.g. to +avoid ABI problems), or if the automatic mapping does not work correctly for +your package, you have to provide dh_python3 with additional rules for the +translation of Python module to Debian package dependencies. + +For a package *python3-foo* that depends on a package *python3-bar*, there are +two files that may provide such rules: + +#. If the *python3-foo* source package ships with a + `debian/py3dist-overrides` file, this file is used by dh_python3 + during the build of *python3-foo*. + +#. If the *python3-bar* source package ships with a + `debian/python3-bar.pydist` file (and uses dh_python3), this file + will be included in the binary package as + `/usr/share/dh-python/dist/cpython3/python3-bar`. During the build + of *python3-foo*, dh_python3 will then find and use the file. + +Both files have the same format described in +`/usr/share/doc/dh-python/README.PyDist`. If all you want is to generate +versioned dependencies (and assuming that the *python3-bar* package provides +the *pybar* Python module), in most cases it will be sufficient to put the line +``pybar python3-bar; PEP386`` into either of the above files. + +private dirs +~~~~~~~~~~~~ +`/usr/share/foo`, `/usr/share/games/foo`, `/usr/lib/foo` and +`/usr/lib/games/foo` private directories are scanned for Python files +by default (where `foo` is binary package name). If your package ships +Python files in some other directory, add another dh_python3 call in +debian/rules with directory name as an argument - you can use different set of +options in this call. If you need to change options (f.e. a list of supported +Python 3 versions) for a private directory that is checked by default, invoke +dh_python3 with --skip-private option and add another call with a path to this +directory and new options. + +debug packages +~~~~~~~~~~~~~~ +In binary packages which name ends with `-dbg`, all files in +`/usr/lib/python3/dist-packages/` directory +that have extensions different than `so` or `h` are removed by default. +Use --no-dbg-cleaning option to disable this feature. + +pyinstall files +~~~~~~~~~~~~~~~ +Files listed in debian/pkg.pyinstall file will be installed as public modules +(i.e. into .../dist-packages/ directory) for all requested Python versions. + +Syntax: ``path/to/file [NAMESPACE] [VERSION_RANGE]`` + +debian directory is automatically removed from the path, so you can place your +files in debian/ directory and install them from this location (if you want to +install them in "debian" namespace, set NAMESPACE to debian). If NAMESPACE is +set, all listed files will be installed in .../dist-packages/NAMESPACE/ +directory. + +Examples: + * ``foo.py`` installs .../dist-packages/foo.py for all supported Python versions + * ``foo/bar.py 3.3-`` installs .../dist-packages/foo/bar.py for versions >= 3.3 + * ``foo/bar.py spam`` installs .../dist-packages/spam/bar.py + * ``debian/*.py spam.egg 3.2`` installs .../python3.2/dist-packages/spam/egg/\*.py + files + +pyremove files +~~~~~~~~~~~~~~ +If you want to remove some public modules (i.e. files in .../dist-packages/ +directory) installed by build system (from all supported Python versions or +only from a subset of these versions), add them to debian/pkg.pyremove file. + +Examples: + * ``*.pth`` removes .pth files from .../dist-packages/ + * ``bar/baz.py 3.2`` removes .../python3.2/dist-packages/bar/baz.py + +bcep files +~~~~~~~~~~ +Byte-compilation exception patterns can be described in these files. Use it if +you want py3compile to skip specific files. This is the only way to skip .py +files in …/dist-packages/ directory (as `--exclude` passed to py3compile in +postinst is not used in rtupdate scripts and thus this option cannot be used +for non-private modules). + +``re|-3.6|/usr/lib/python3/dist-packages/jinja2|.*/async(foo|bar).py`` +will skip byte-compilation of `asyncfoo.py` and `asyncbar.py` in +`/usr/lib/python3/dist-packages/jinja2/` directory for each interpreter that +doesn't support `async` keyword (introduced in Python 3.6). + +If you want to skip byte-compilation in a subdirectory for all interpreters, use: +``dir|-4.0|/usr/lib/python3/dist-packages/foo/tests/``. +VERSION_RANGE (`-4.0` in the example) is described in `README.PyDist` file. + +`debian/python3-foo.bcep` file from source package will be included in the +binary package as `/usr/share/python3/bcep/python3-foo.bcep` + +overriding supported / default Python versions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +If you want to override system's list of supported Python versions or the +default one (f.e. to build a package that includes symlinks for older version +of Python or compile .py files only for given interpreter version), you can do +that via `DEBPYTHON3_SUPPORTED` and/or `DEBPYTHON3_DEFAULT` env. variables. + +Example: ``3.2,3.3`` limits the list of supported Python versions to Python 3.2 +and Python 3.3. + + +OPTIONS +======= +--version show program's version number and exit + +-h, --help show help message and exit + +--no-guessing-deps disable guessing dependencies + +--no-dbg-cleaning do not remove any files from debug packages + +--no-ext-rename do not add magic tags nor multiarch tuples to extension file names + +--no-shebang-rewrite do not rewrite shebangs + +--skip-private don't check private directories + +-v, --verbose turn verbose mode on + +-i, --indep act on architecture independent packages + +-a, --arch act on architecture dependent packages + +-q, --quiet be quiet + +-p PACKAGE, --package=PACKAGE act on the package named PACKAGE + +-N NO_PACKAGE, --no-package=NO_PACKAGE do not act on the specified package + +--remaining-packages Do not act on the packages which have already + been acted on by this command earlier in the same override. + +-V VERSION_RANGE specify list of supported Python 3 versions. See + py3compile(1) for examples + +-X REGEXPR, --exclude=REGEXPR exclude items that match given REGEXPR. You may + use this option multiple times to build up a list of things to exclude from + byte-compilation in private dirs. See also `bcep files`. + +--compile-all compile all files from given private directory in postinst/rtupdate + not just the ones provided by the package (i.e. do not pass the --package + parameter to py3compile/py3clean) + +--accept-upstream-versions accept upstream versions while translating + Python dependencies into Debian ones + +--depends=DEPENDS translate given requirements into Debian dependencies + and add them to ${python3:Depends}. Use it for missing items in + `requires.txt` / `Requires-Dist`. + +--depends-section=SECTION translate requirements from given extra + sections of `requres.txt` / `Requires-Dist` into Debian dependencies + and add them to ${python3:Depends}. May be repeated for multiple + sections. + +--recommends=RECOMMENDS translate given requirements into Debian dependencies + and add them to ${python3:Recommends} + +--recommends-section=SECTION translate requirements from given extra + sections of `requires.txt` / `Requires-Dist` into Debian dependencies + and add them to ${python3:Recommends}. May be repeated for multiple + sections. + +--suggests=SUGGESTS translate given requirements into Debian dependencies + and add them to ${python3:Suggests} + +--suggests-section=SECTION translate requirements from given extra + sections of `requires.txt` / `Requires-Dist` into Debian dependencies + and add them to ${python3:Suggests}. May be repeated for multiple + sections. + +--requires=FILENAME translate requirements from given file(s) into Debian + dependencies and add them to ${python3:Depends} + +--shebang=COMMAND use given command as shebang in scripts + +--ignore-shebangs do not translate shebangs into Debian dependencies + +SEE ALSO +======== +* /usr/share/doc/python3/python-policy.txt.gz +* /usr/share/doc/dh-python/README.PyDist +* pybuild(1) +* py3compile(1), py3clean(1) +* pycompile(1), pyclean(1) +* http://deb.li/dhp3 - most recent version of this document diff --git a/dhpython/__init__.py b/dhpython/__init__.py new file mode 100644 index 0000000..338eb17 --- /dev/null +++ b/dhpython/__init__.py @@ -0,0 +1,113 @@ +# Copyright © 2010-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 re + +PKG_PREFIX_MAP = {'cpython2': 'python', + 'cpython3': 'python3', + 'pypy': 'pypy'} + +# minimum version required for compile/clean scripts: +MINPYCDEP = {'cpython2': 'python2:any', + 'cpython3': 'python3:any', + 'pypy': 'pypy'} + +PUBLIC_DIR_RE = { + 'cpython2': re.compile(r'.*?/usr/lib/python(2\.\d)(?:/|$)'), + 'cpython3': re.compile(r'.*?/usr/lib/python(3(?:\.\d+)?)(?:/|$)'), + 'pypy': re.compile(r'.*?/usr/lib/pypy(?:/|$)')} + +INTERPRETER_DIR_TPLS = { + 'cpython2': r'.*/python2\.\d/', + 'cpython3': r'.*/python3(?:\.\d+)?/', + 'pypy': r'.*/pypy/'} + +MULTIARCH_DIR_TPL = re.compile( + '.*/([a-z][^/-]+-(?:linux|kfreebsd|gnu)(?:-[^/-]+)?)(?:/.*|$)') + +# Interpreter site-directories +OLD_SITE_DIRS = { + 'cpython2': [ + '/usr/local/lib/python{}/site-packages', + '/usr/local/lib/python{}/dist-packages', + '/var/lib/python-support/python{}', + '/usr/lib/pymodules/python{}', + lambda version: '/usr/lib/python{}/site-packages'.format(version) + if version >= '2.6' else None], + 'cpython3': [ + '/usr/local/lib/python{}/site-packages', + '/usr/local/lib/python{}/dist-packages', + '/usr/lib/python{}/site-packages', + '/usr/lib/python{}/dist-packages', + '/var/lib/python-support/python{}', + '/usr/lib/pymodules/python{}'], + 'pypy': [ + '/usr/local/lib/pypy/site-packages', + '/usr/local/lib/pypy/dist-packages', + '/usr/lib/pypy/site-packages']} + +# PyDist related +PYDIST_DIRS = { + 'cpython2': '/usr/share/python/dist/', + 'cpython3': '/usr/share/python3/dist/', + 'pypy': '/usr/share/pypy/dist/'} + +PYDIST_OVERRIDES_FNAMES = { + 'cpython2': 'debian/pydist-overrides', + 'cpython3': 'debian/py3dist-overrides', + 'pypy': 'debian/pypydist-overrides'} + +PYDIST_DPKG_SEARCH_TPLS = { + # implementation: (dpkg -S query, regex filter) + 'cpython2': ('*/{}-?*.*-info', + r'/(python2\..|pyshared)/.*.(egg|dist)-info$'), + 'cpython3': ('*python3/*/{}-?*.*-info', r'.(egg|dist)-info$'), + 'pypy': ('*/pypy/dist-packages/{}-?*.*-info', r'.(egg|dist)-info$'), +} + +# DebHelper related +DEPENDS_SUBSTVARS = { + 'cpython2': '${python:Depends}', + 'cpython3': '${python3:Depends}', + 'pypy': '${pypy:Depends}', +} +PKG_NAME_TPLS = { + 'cpython2': ('python-', 'python2.'), + 'cpython3': ('python3-', 'python3.'), + 'pypy': ('pypy-',) +} +RT_LOCATIONS = { + 'cpython2': '/usr/share/python/runtime.d/', + 'cpython3': '/usr/share/python3/runtime.d/', + 'pypy': '/usr/share/pypy/runtime.d/', +} +RT_TPLS = { + 'cpython2': ''' +if [ "$1" = rtupdate ]; then +\tpyclean {pkg_arg} {dname} +\tpycompile {pkg_arg} {args} {dname} +fi''', + 'cpython3': ''' +if [ "$1" = rtupdate ]; then +\tpy3clean {pkg_arg} {dname} +\tpy3compile {pkg_arg} {args} {dname} +fi''', + 'pypy': '' +} diff --git a/dhpython/_defaults.py b/dhpython/_defaults.py new file mode 100755 index 0000000..cf7537b --- /dev/null +++ b/dhpython/_defaults.py @@ -0,0 +1,99 @@ +#! /usr/bin/python3 +# Copyright © 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 +from configparser import ConfigParser +from os import environ +from os.path import exists +from subprocess import Popen, PIPE + +SUPPORTED = { + 'cpython2': [(2, 7)], + 'cpython3': [(3, 8)], + 'pypy': [(4, 0)]} +DEFAULT = { + 'cpython2': (2, 7), + 'cpython3': (3, 8), + 'pypy': (4, 0)} + +log = logging.getLogger('dhpython') + + +def cpython_versions(major): + result = [None, None] + ver = '' if major == 2 else '3' + supported = environ.get("DEBPYTHON{}_SUPPORTED".format(ver)) + default = environ.get("DEBPYTHON{}_DEFAULT".format(ver)) + if not supported or not default: + config = ConfigParser() + config.read("/usr/share/python{}/debian_defaults".format(ver)) + if not default: + default = config.get('DEFAULT', 'default-version', fallback='')[6:] + if not supported: + supported = config.get('DEFAULT', 'supported-versions', fallback='')\ + .replace('python', '') + if default: + try: + result[0] = tuple(int(i) for i in default.split('.')) + except Exception as err: + log.warn('invalid debian_defaults file: %s', err) + if supported: + try: + result[1] = tuple(tuple(int(j) for j in i.strip().split('.')) + for i in supported.split(',')) + except Exception as err: + log.warn('invalid debian_defaults file: %s', err) + return result + + +def from_file(fpath): + if not exists(fpath): + raise ValueError("missing interpreter: %s" % fpath) + command = "{} --version".format(fpath) + with Popen(command, shell=True, stdout=PIPE) as process: + stdout, stderr = process.communicate() + stdout = str(stdout, 'utf-8') + + print(stdout) + + +cpython2 = cpython_versions(2) +cpython3 = cpython_versions(3) +if cpython2[0]: + DEFAULT['cpython2'] = cpython2[0] +if cpython3[0]: + DEFAULT['cpython3'] = cpython3[0] +if cpython2[1]: + SUPPORTED['cpython2'] = cpython2[1] +if cpython3[1]: + SUPPORTED['cpython3'] = cpython3[1] +#from_file('/usr/bin/pypy') + + +if __name__ == '__main__': + from sys import argv, stderr + if len(argv) != 3: + print('invalid number of arguments', file=stderr) + exit(1) + if argv[1] == 'default': + print('.'.join(str(i) for i in DEFAULT[argv[2]])) + elif argv[1] == 'supported': + print(','.join(('.'.join(str(i) for i in v) for v in SUPPORTED[argv[2]]))) diff --git a/dhpython/build/__init__.py b/dhpython/build/__init__.py new file mode 100644 index 0000000..d350ccb --- /dev/null +++ b/dhpython/build/__init__.py @@ -0,0 +1,42 @@ +# 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 +from glob import glob1 +from os.path import dirname + +from dhpython.exceptions import RequiredCommandMissingException + +log = logging.getLogger('dhpython') + +plugins = {} +for i in sorted(i[7:-3] for i in glob1(dirname(__file__), 'plugin_*.py')): + try: + module = __import__("dhpython.build.plugin_%s" % i, fromlist=[i]) + module.BuildSystem.NAME = i + module.BuildSystem.is_usable() + plugins[i] = module.BuildSystem + except RequiredCommandMissingException as err: + log.debug("cannot initialize '%s' plugin: Missing command '%s'", i, err) + except Exception as err: + if log.level < logging.INFO: + log.debug("cannot initialize '%s' plugin", i, exc_info=True) + else: + log.debug("cannot initialize '%s' plugin: %s", i, err) diff --git a/dhpython/build/base.py b/dhpython/build/base.py new file mode 100644 index 0000000..427ef2e --- /dev/null +++ b/dhpython/build/base.py @@ -0,0 +1,293 @@ +# 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 +from functools import wraps +from glob import glob1 +from os import remove, walk +from os.path import exists, isdir, join +from subprocess import Popen, PIPE +from shutil import rmtree, copyfile, copytree +from dhpython.exceptions import RequiredCommandMissingException +from dhpython.tools import execute +try: + from shlex import quote +except ImportError: + # shlex.quote is new in Python 3.3 + def quote(s): + if not s: + return "''" + return "'" + s.replace("'", "'\"'\"'") + "'" + +log = logging.getLogger('dhpython') + + +def copy_test_files(dest='{build_dir}', + filelist='{home_dir}/testfiles_to_rm_before_install', + add_to_args=('test', 'tests')): + + def _copy_test_files(func): + + @wraps(func) + def __copy_test_files(self, context, args, *oargs, **kwargs): + files_to_copy = {'test', 'tests'} + # check debian/pybuild_pythonX.Y.testfiles + for tpl in ('_{i}{v}', '_{i}{m}', ''): + tpl = tpl.format(i=args['interpreter'].name, + v=args['version'], + m=args['version'].major) + fpath = join(args['dir'], 'debian/pybuild{}.testfiles'.format(tpl)) + if exists(fpath): + with open(fpath, encoding='utf-8') as fp: + # overwrite files_to_copy if .testfiles file found + files_to_copy = [line.strip() for line in fp.readlines() + if not line.startswith('#')] + break + + files_to_remove = set() + for name in files_to_copy: + src_dpath = join(args['dir'], name) + dst_dpath = join(dest.format(**args), name.rsplit('/', 1)[-1]) + if exists(src_dpath): + if not exists(dst_dpath): + if isdir(src_dpath): + copytree(src_dpath, dst_dpath) + else: + copyfile(src_dpath, dst_dpath) + files_to_remove.add(dst_dpath + '\n') + if not args['args'] and 'PYBUILD_TEST_ARGS' not in context['ENV']\ + and (self.cfg.test_pytest or self.cfg.test_nose) \ + and name in add_to_args: + args['args'] = name + if files_to_remove and filelist: + with open(filelist.format(**args), 'a') as fp: + fp.writelines(files_to_remove) + + return func(self, context, args, *oargs, **kwargs) + return __copy_test_files + return _copy_test_files + + +class Base: + """Base class for build system plugins + + :attr REQUIRED_COMMANDS: list of command checked by default in :meth:is_usable, + if one of them is missing, plugin cannot be used. + :type REQUIRED_COMMANDS: list of strings + :attr REQUIRED_FILES: list of files (or glob templates) required by given + build system + :attr OPTIONAL_FILES: dictionary of glob templates (key) and score (value) + used to detect if given plugin is the best one for the job + :type OPTIONAL_FILES: dict (key is a string, value is an int) + :attr SUPPORTED_INTERPRETERS: set of interpreter templates (with or without + {version}) supported by given plugin + """ + DESCRIPTION = '' + REQUIRED_COMMANDS = [] + REQUIRED_FILES = [] + OPTIONAL_FILES = {} + SUPPORTED_INTERPRETERS = {'python', 'python3', 'python-dbg', 'python3-dbg', + 'python{version}', 'python{version}-dbg'} + # files and directories to remove during clean step (other than .pyc): + CLEAN_FILES = {'.pytest_cache', '.coverage'} + + def __init__(self, cfg): + self.cfg = cfg + + def __repr__(self): + return "BuildSystem(%s)" % self.NAME + + @classmethod + def is_usable(cls): + for command in cls.REQUIRED_COMMANDS: + process = Popen(['which', command], stdout=PIPE, stderr=PIPE) + out, err = process.communicate() + if process.returncode != 0: + raise RequiredCommandMissingException(command) + + def detect(self, context): + """Return certainty level that this plugin describes the right build system + + This method is using cls.{REQUIRED,OPTIONAL}_FILES only by default, + please extend it in the plugin if more sofisticated methods can be used + for given build system. + + :return: 0 <= certainty <= 100 + :rtype: int + """ + result = 0 + + required_files_num = 0 + self.DETECTED_REQUIRED_FILES = {} # can be used in the plugin later + for tpl in self.REQUIRED_FILES: + found = False + for ftpl in tpl.split('|'): + res = glob1(context['dir'], ftpl) + if res: + found = True + self.DETECTED_REQUIRED_FILES.setdefault(tpl, []).extend(res) + if found: + required_files_num += 1 + # add max 50 points depending on how many required files are available + if self.REQUIRED_FILES: + result += int(required_files_num / len(self.REQUIRED_FILES) * 50) + + self.DETECTED_OPTIONAL_FILES = {} + for ftpl, score in self.OPTIONAL_FILES.items(): + res = glob1(context['dir'], ftpl) + if res: + result += score + self.DETECTED_OPTIONAL_FILES.setdefault(ftpl, []).extend(res) + if result > 100: + return 100 + return result + + def clean(self, context, args): + if self.cfg.test_tox: + tox_dir = join(args['dir'], '.tox') + if isdir(tox_dir): + try: + rmtree(tox_dir) + except Exception: + log.debug('cannot remove %s', tox_dir) + + for fn in self.CLEAN_FILES: + path = join(context['dir'], fn) + if isdir(path): + try: + rmtree(path) + except Exception: + log.debug('cannot remove %s', path) + elif exists(path): + try: + remove(path) + except Exception: + log.debug('cannot remove %s', path) + + for root, dirs, file_names in walk(context['dir']): + for name in dirs: + if name == '__pycache__': + dpath = join(root, name) + log.debug('removing dir: %s', dpath) + try: + rmtree(dpath) + except Exception: + log.debug('cannot remove %s', dpath) + else: + dirs.remove(name) + for fn in file_names: + if fn.endswith(('.pyc', '.pyo')): + fpath = join(root, fn) + log.debug('removing: %s', fpath) + try: + remove(fpath) + except Exception: + log.debug('cannot remove %s', fpath) + + def configure(self, context, args): + raise NotImplementedError("configure method not implemented in %s" % self.NAME) + + def install(self, context, args): + raise NotImplementedError("install method not implemented in %s" % self.NAME) + + def build(self, context, args): + raise NotImplementedError("build method not implemented in %s" % self.NAME) + + @copy_test_files() + def test(self, context, args): + if self.cfg.test_nose2: + return 'cd {build_dir}; {interpreter} -m nose2 -v {args}' + elif self.cfg.test_nose: + return 'cd {build_dir}; {interpreter} -m nose -v {args}' + elif self.cfg.test_pytest: + return 'cd {build_dir}; {interpreter} -m pytest {args}' + elif self.cfg.test_tox: + # tox will call pip to install the module. Let it install the + # module inside the virtualenv + pydistutils_cfg = join(args['home_dir'], '.pydistutils.cfg') + if exists(pydistutils_cfg): + remove(pydistutils_cfg) + return 'cd {build_dir}; tox -c {dir}/tox.ini --sitepackages -e py{version.major}{version.minor} {args}' + elif self.cfg.test_custom: + return 'cd {build_dir}; {args}' + elif args['version'] == '2.7' or args['version'] >> '3.1' or args['interpreter'] == 'pypy': + return 'cd {build_dir}; {interpreter} -m unittest discover -v {args}' + + def execute(self, context, args, command, log_file=None): + if log_file is False and self.cfg.really_quiet: + log_file = None + command = command.format(**args) + env = dict(context['ENV']) + if 'ENV' in args: + env.update(args['ENV']) + log.info(command) + return execute(command, context['dir'], env, log_file) + + def print_args(self, context, args): + cfg = self.cfg + if len(cfg.print_args) == 1 and len(cfg.interpreter) == 1 and '{version}' not in cfg.interpreter[0]: + i = cfg.print_args[0] + if '{' in i: + print(i.format(**args)) + else: + print(args.get(i, '')) + else: + for i in cfg.print_args: + if '{' in i: + print(i.format(**args)) + else: + print('{} {}: {}'.format(args['interpreter'], i, args.get(i, ''))) + + +def shell_command(func): + + @wraps(func) + def wrapped_func(self, context, args, *oargs, **kwargs): + command = kwargs.pop('command', None) + if not command: + command = func(self, context, args, *oargs, **kwargs) + if isinstance(command, int): # final result + return command + if not command: + log.warn('missing command ' + '(plugin=%s, method=%s, interpreter=%s, version=%s)', + self.NAME, func.__name__, + args.get('interpreter'), args.get('version')) + return command + + if self.cfg.quiet: + log_file = join(args['home_dir'], '{}_cmd.log'.format(func.__name__)) + else: + log_file = False + + quoted_args = dict((k, quote(v)) if k in ('dir', 'destdir') + or k.endswith('_dir') else (k, v) + for k, v in args.items()) + command = command.format(**quoted_args) + + output = self.execute(context, args, command, log_file) + if output['returncode'] != 0: + msg = 'exit code={}: {}'.format(output['returncode'], command) + if log_file: + msg += '\nfull command log is available in {}'.format(log_file) + raise Exception(msg) + return True + + return wrapped_func diff --git a/dhpython/build/plugin_autopkgtest.py b/dhpython/build/plugin_autopkgtest.py new file mode 100644 index 0000000..b9a76e7 --- /dev/null +++ b/dhpython/build/plugin_autopkgtest.py @@ -0,0 +1,38 @@ +# vim: et ts=4 sw=4 +# Copyright © 2021 Antonio Terceiro <terceiro@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. + +from dhpython.build.base import Base, shell_command + + +class BuildSystem(Base): + """ + autopkgtest test runner. All the methods that are not related to + testing (configure, build, clean etc) are not implemented by this class, on + purpose. + """ + + NAME = "autopkgtest" + DESCRIPTION = 'autopkgtest test runner' + SUPPORTED_INTERPRETERS = {'python3', 'python{version}'} + + @shell_command + def test(self, context, args): + return super().test(context, args) diff --git a/dhpython/build/plugin_cmake.py b/dhpython/build/plugin_cmake.py new file mode 100644 index 0000000..7cf52e9 --- /dev/null +++ b/dhpython/build/plugin_cmake.py @@ -0,0 +1,71 @@ +# 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. + +from dhpython.build.base import Base, shell_command, copy_test_files + + +class BuildSystem(Base): + DESCRIPTION = 'CMake build system (using dh_auto_* commands)' + REQUIRED_COMMANDS = ['cmake'] + REQUIRED_FILES = ['CMakeLists.txt'] + OPTIONAL_FILES = {'cmake_uninstall.cmake': 10, 'CMakeCache.txt': 10} + + @shell_command + def clean(self, context, args): + super(BuildSystem, self).clean(context, args) + return 'dh_auto_clean --buildsystem=cmake' + + @shell_command + def configure(self, context, args): + return ('dh_auto_configure --buildsystem=cmake' + ' --builddirectory={build_dir} --' + # FindPythonInterp: + ' -DPYTHON_EXECUTABLE:FILEPATH=/usr/bin/{interpreter}' + ' -DPYTHON_LIBRARY:FILEPATH={interpreter.library_file}' + ' -DPYTHON_INCLUDE_DIR:PATH={interpreter.include_dir}' + # FindPython: + ' -DPython_EXECUTABLE=/usr/bin/{interpreter}' + ' -DPython_LIBRARY={interpreter.library_file}' + ' -DPython_INCLUDE_DIR={interpreter.include_dir}' + # FindPython3: + ' -DPython3_EXECUTABLE=/usr/bin/{interpreter}' + ' -DPython3_LIBRARY={interpreter.library_file}' + ' -DPython3_INCLUDE_DIR={interpreter.include_dir}' + ' {args}') + + @shell_command + def build(self, context, args): + return ('dh_auto_build --buildsystem=cmake' + ' --builddirectory={build_dir}' + ' -- {args}') + + @shell_command + def install(self, context, args): + return ('dh_auto_install --buildsystem=cmake' + ' --builddirectory={build_dir}' + ' --destdir={destdir}' + ' -- {args}') + + @shell_command + @copy_test_files() + def test(self, context, args): + return ('dh_auto_test --buildsystem=cmake' + ' --builddirectory={build_dir}' + ' -- {args}') diff --git a/dhpython/build/plugin_custom.py b/dhpython/build/plugin_custom.py new file mode 100644 index 0000000..8b317f2 --- /dev/null +++ b/dhpython/build/plugin_custom.py @@ -0,0 +1,48 @@ +# 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. + +from dhpython.build.base import Base, shell_command, copy_test_files + + +class BuildSystem(Base): + DESCRIPTION = 'use --*-args options to configure this system' + SUPPORTED_INTERPRETERS = True # all interpreters + + @shell_command + def clean(self, context, args): + super(BuildSystem, self).clean(context, args) + return args['args'] + + @shell_command + def configure(self, context, args): + return args['args'] + + @shell_command + def build(self, context, args): + return args['args'] + + @shell_command + def install(self, context, args): + return args['args'] + + @shell_command + @copy_test_files() + def test(self, context, args): + return args['args'] or super(BuildSystem, self).test(context, args) diff --git a/dhpython/build/plugin_distutils.py b/dhpython/build/plugin_distutils.py new file mode 100644 index 0000000..342ec6f --- /dev/null +++ b/dhpython/build/plugin_distutils.py @@ -0,0 +1,121 @@ +# 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 +from glob import glob1 +from os import remove +from os.path import exists, isdir, join +from shutil import rmtree +from dhpython.build.base import Base, shell_command, copy_test_files + +log = logging.getLogger('dhpython') +_setup_tpl = 'setup.py|setup-3.py' + + +def create_pydistutils_cfg(func): + """distutils doesn't have sane command-line API - this decorator creates + .pydistutils.cfg file to workaround it + + hint: if you think this is plain stupid, please don't read + distutils/setuptools/distribute sources + """ + + def wrapped_func(self, context, args, *oargs, **kwargs): + fpath = join(args['home_dir'], '.pydistutils.cfg') + if not exists(fpath): + with open(fpath, 'w', encoding='utf-8') as fp: + lines = ['[clean]\n', + 'all=1\n', + '[build]\n', + 'build_lib={}\n'.format(args['build_dir']), + '[install]\n', + 'force=1\n', + 'install_layout=deb\n', + 'install_scripts=$base/bin\n', + 'install_lib={}\n'.format(args['install_dir']), + 'prefix=/usr\n'] + log.debug('pydistutils config file:\n%s', ''.join(lines)) + fp.writelines(lines) + context['ENV']['HOME'] = args['home_dir'] + return func(self, context, args, *oargs, **kwargs) + + wrapped_func.__name__ = func.__name__ + return wrapped_func + + +class BuildSystem(Base): + DESCRIPTION = 'Distutils build system' + SUPPORTED_INTERPRETERS = {'python', 'python3', 'python{version}', + 'python-dbg', 'python3-dbg', 'python{version}-dbg', + 'pypy'} + REQUIRED_FILES = [_setup_tpl] + OPTIONAL_FILES = {'setup.cfg': 1, + 'requirements.txt': 1, + 'PKG-INFO': 10, + '*.egg-info': 10} + CLEAN_FILES = Base.CLEAN_FILES | {'build'} + + def detect(self, context): + result = super(BuildSystem, self).detect(context) + if _setup_tpl in self.DETECTED_REQUIRED_FILES: + context['args']['setup_py'] = self.DETECTED_REQUIRED_FILES[_setup_tpl][0] + else: + context['args']['setup_py'] = 'setup.py' + return result + + @shell_command + @create_pydistutils_cfg + def clean(self, context, args): + super(BuildSystem, self).clean(context, args) + if exists(args['interpreter'].binary()): + return '{interpreter} {setup_py} clean {args}' + return 0 # no need to invoke anything + + @shell_command + @create_pydistutils_cfg + def configure(self, context, args): + return '{interpreter} {setup_py} config {args}' + + @shell_command + @create_pydistutils_cfg + def build(self, context, args): + return '{interpreter.binary_dv} {setup_py} build {args}' + + @shell_command + @create_pydistutils_cfg + def install(self, context, args): + # remove egg-info dirs from build_dir + for fname in glob1(args['build_dir'], '*.egg-info'): + fpath = join(args['build_dir'], fname) + rmtree(fpath) if isdir(fpath) else remove(fpath) + + return '{interpreter.binary_dv} {setup_py} install --root {destdir} {args}' + + @shell_command + @create_pydistutils_cfg + @copy_test_files() + def test(self, context, args): + if not self.cfg.custom_tests: + fpath = join(args['dir'], args['setup_py']) + with open(fpath, 'rb') as fp: + if fp.read().find(b'test_suite') > 0: + # TODO: is that enough to detect if test target is available? + return '{interpreter} {setup_py} test {args}' + return super(BuildSystem, self).test(context, args) diff --git a/dhpython/build/plugin_flit.py b/dhpython/build/plugin_flit.py new file mode 100644 index 0000000..004d657 --- /dev/null +++ b/dhpython/build/plugin_flit.py @@ -0,0 +1,170 @@ +# Copyright © 2012-2020 Piotr Ożarowski <piotr@debian.org> +# © 2020 Scott Kitterman <scott@kitterman.com> +# +# 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. + +from fnmatch import fnmatch +from pathlib import Path +import copy +import csv +import logging +import os +import os.path as osp +import shutil +import sysconfig +try: + import tomli +except ModuleNotFoundError: + # Plugin still works, only needed for autodetection + pass +try: + from flit.install import Installer +except ImportError: + Installer = object + +from dhpython.build.base import Base, shell_command + +log = logging.getLogger('dhpython') + + +class DebianInstaller(Installer): + def install_directly(self, destdir, installdir): + """Install a module/package into package directory, and create its + scripts. + """ + if installdir[:1] == os.sep: + installdir = installdir[1:] + + vars_ = copy.copy(sysconfig.get_config_vars()) + vars_['base'] = destdir + vars_['base'] + try: + dirs = sysconfig.get_paths(scheme='deb_system', vars=vars_) + except KeyError: + # Debian hasn't patched sysconfig schemes until 3.10 + # TODO: Introduce a version check once sysconfig is patched. + dirs = sysconfig.get_paths(scheme='posix_prefix', vars=vars_) + + dirs['purelib'] = dirs['platlib'] = osp.join(destdir, installdir) + os.makedirs(dirs['purelib'], exist_ok=True) + os.makedirs(dirs['scripts'], exist_ok=True) + + dst = osp.join(dirs['purelib'], osp.basename(self.module.path)) + if osp.lexists(dst): + if osp.isdir(dst) and not osp.islink(dst): + shutil.rmtree(dst) + else: + os.unlink(dst) + + src = str(self.module.path) + if self.module.is_package: + log.info("Installing package %s -> %s", src, dst) + shutil.copytree(src, dst) + self._record_installed_directory(dst) + else: + log.info("Installing file %s -> %s", src, dst) + shutil.copy2(src, dst) + self.installed_files.append(dst) + + scripts = self.ini_info.entrypoints.get('console_scripts', {}) + if scripts: + log.info("Installing scripts to %s", dirs['scripts']) + self.install_scripts(scripts, dirs['scripts']) + + log.info("Writing dist-info %s", dirs['purelib']) + self.write_dist_info(dirs['purelib']) + # Remove direct_url.json - contents are not useful or reproduceable + for path in Path(dirs['purelib']).glob("*.dist-info/direct_url.json"): + path.unlink() + # Remove build path from RECORD files + for path in Path(dirs['purelib']).glob("*.dist-info/RECORD"): + with open(path) as f: + reader = csv.reader(f) + records = list(reader) + with open(path, 'w') as f: + writer = csv.writer(f) + for path, hash_, size in records: + path = path.replace(destdir, '') + if fnmatch(path, "*.dist-info/direct_url.json"): + continue + writer.writerow([path, hash_, size]) + + +class BuildSystem(Base): + DESCRIPTION = 'Flit build system' + SUPPORTED_INTERPRETERS = {'python3', 'python{version}'} + REQUIRED_FILES = ['pyproject.toml'] + OPTIONAL_FILES = {} + CLEAN_FILES = Base.CLEAN_FILES | {'build'} + + def detect(self, context): + """Return certainty level that this plugin describes the right build + system + + This method uses cls.{REQUIRED}_FILES (pyroject.toml) as well as + checking to see if build-backend is set to flit_core.buildapi. + + Score is 85 if both are present (to allow manually setting distutils to + score higher if set). + + :return: 0 <= certainty <= 100 + :rtype: int + """ + if Installer is object: + return 0 + + result = super().detect(context) + if result > 100: + return 100 + return result + + def clean(self, context, args): + super().clean(context, args) + if osp.exists(args['interpreter'].binary()): + log.debug("removing '%s' (and everything under it)", + args['build_dir']) + osp.isdir(args['build_dir']) and shutil.rmtree(args['build_dir']) + return 0 # no need to invoke anything + + def configure(self, context, args): + # Flit does not support binary extensions + return 0 # Not needed for flit + + def build(self, context, args): + log.warning("The pybuild flit plugin is deprecated, " + "please use the pyproject plugin instead.") + my_dir = Path(args['dir']) + install_kwargs = {'user': False, 'symlink': False, 'deps': 'none'} + DebianInstaller.from_ini_path(my_dir / 'pyproject.toml', + **install_kwargs).install_directly( + args['build_dir'], '') + # These get byte compiled too, although it's not logged. + return 0 # Not needed for flit + + def install(self, context, args): + my_dir = Path(args['dir']) + install_kwargs = {'user': False, 'symlink': False, 'deps': 'none'} + DebianInstaller.from_ini_path(my_dir / 'pyproject.toml', + **install_kwargs).install_directly( + args['destdir'], + args['install_dir']) + return 0 # Not needed for flit' + + @shell_command + def test(self, context, args): + return super().test(context, args) diff --git a/dhpython/build/plugin_pyproject.py b/dhpython/build/plugin_pyproject.py new file mode 100644 index 0000000..b24aa9a --- /dev/null +++ b/dhpython/build/plugin_pyproject.py @@ -0,0 +1,201 @@ +# Copyright © 2012-2020 Piotr Ożarowski <piotr@debian.org> +# © 2020 Scott Kitterman <scott@kitterman.com> +# © 2021 Stuart Prescott <stuart@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. + +from pathlib import Path +import logging +import os.path as osp +import shutil +import sysconfig +try: + import tomli +except ModuleNotFoundError: + # Plugin still works, only needed for autodetection + pass +try: + from installer import install + from installer.destinations import SchemeDictionaryDestination + from installer.sources import WheelFile +except ModuleNotFoundError: + SchemeDictionaryDestination = WheelFile = install = None + +from dhpython.build.base import Base, shell_command + +log = logging.getLogger('dhpython') + + +class BuildSystem(Base): + DESCRIPTION = 'Generic PEP517 build system' + SUPPORTED_INTERPRETERS = {'python3', 'python{version}'} + REQUIRED_FILES = ['pyproject.toml'] + OPTIONAL_FILES = {} + CLEAN_FILES = Base.CLEAN_FILES | {'build'} + + def detect(self, context): + """Return certainty level that this plugin describes the right build + system + + This method uses cls.{REQUIRED}_FILES (pyroject.toml) only; any + other PEP517 compliant builder (such as the flit) builder should + indicate higher specificity than this plugin. + + :return: 0 <= certainty <= 100 + :rtype: int + """ + result = super().detect(context) + # Temporarily reduce the threshold while we're in beta + result -= 20 + + try: + with open('pyproject.toml', 'rb') as f: + pyproject = tomli.load(f) + if pyproject.get('build-system', {}).get('build-backend'): + result += 10 + else: + # Not a PEP517 built package + result = 0 + except NameError: + # No toml, no autdetection + result = 0 + except FileNotFoundError: + # Not a PEP517 package + result = 0 + if result > 100: + return 100 + return result + + def clean(self, context, args): + super().clean(context, args) + if osp.exists(args['interpreter'].binary()): + log.debug("removing '%s' (and everything under it)", + args['build_dir']) + osp.isdir(args['build_dir']) and shutil.rmtree(args['build_dir']) + return 0 # no need to invoke anything + + def configure(self, context, args): + if install is None: + raise Exception("PEP517 plugin dependencies are not available. " + "Please Build-Depend on pybuild-plugin-pyproject.") + # No separate configure step + return 0 + + def build(self, context, args): + self.build_step1(context, args) + self.build_step2(context, args) + + @shell_command + def build_step1(self, context, args): + """ build a wheel using the PEP517 builder defined by upstream """ + log.info('Building wheel for %s with "build" module', + args['interpreter']) + context['ENV']['FLIT_NO_NETWORK'] = '1' + context['ENV']['HOME'] = args['home_dir'] + return ('{interpreter} -m build ' + '--skip-dependency-check --no-isolation --wheel ' + '--outdir ' + args['home_dir'] + + ' {args}' + ) + + def build_step2(self, context, args): + """ unpack the wheel into pybuild's normal """ + log.info('Unpacking wheel built for %s with "installer" module', + args['interpreter']) + extras = {} + for extra in ('scripts', 'data'): + path = Path(args["home_dir"]) / extra + if osp.exists(path): + log.warning(f'{extra.title()} directory already exists, ' + 'skipping unpack. ' + 'Is the Python package being built twice?') + return + extras[extra] = path + destination = SchemeDictionaryDestination( + { + 'platlib': args['build_dir'], + 'purelib': args['build_dir'], + 'scripts': extras['scripts'], + 'data': extras['data'], + }, + interpreter=args['interpreter'].binary_dv, + script_kind='posix', + ) + + # FIXME this next step will unpack every single wheel file it finds + # which is probably ok since each wheel is built in a separate + # directory; but perhaps it should only accept the correctly named + # wheels that match the current interpreter? + # python-packaging has relevant utilities in + # - packaging/utils.py::parse_wheel_filename + # - packaging/tags.py (although it is current-interpreter-centric) + wheels = Path(args['home_dir']).glob('*.whl') + for wheel in wheels: + if wheel.name.startswith('UNKNOWN'): + raise Exception(f'UNKNOWN wheel found: {wheel.name}. Does ' + 'pyproject.toml specify a build-backend?') + with WheelFile.open(wheel) as source: + install( + source=source, + destination=destination, + additional_metadata={}, + ) + + def install(self, context, args): + log.info('Copying package built for %s to destdir', + args['interpreter']) + try: + paths = sysconfig.get_paths(scheme='deb_system') + except KeyError: + # Debian hasn't patched sysconfig schemes until 3.10 + # TODO: Introduce a version check once sysconfig is patched. + paths = sysconfig.get_paths(scheme='posix_prefix') + + # start by copying the data and scripts + for extra in ('data', 'scripts'): + src_dir = Path(args['home_dir']) / extra + if not src_dir.exists(): + continue + target_dir = args['destdir'] + paths[extra] + log.debug('Copying %s directory contents from %s -> %s', + extra, src_dir, target_dir) + shutil.copytree( + src_dir, + target_dir, + dirs_exist_ok=True, + ) + + # then copy the modules + module_dir = args['build_dir'] + target_dir = args['destdir'] + args['install_dir'] + log.debug('Copying module contents from %s -> %s', + module_dir, target_dir) + shutil.copytree( + module_dir, + target_dir, + dirs_exist_ok=True, + ) + + @shell_command + def test(self, context, args): + scripts = Path(args["home_dir"]) / 'scripts' + if scripts.exists(): + context['ENV']['PATH'] = f"{scripts}:{context['ENV']['PATH']}" + context['ENV']['HOME'] = args['home_dir'] + return super().test(context, args) diff --git a/dhpython/debhelper.py b/dhpython/debhelper.py new file mode 100644 index 0000000..f497a60 --- /dev/null +++ b/dhpython/debhelper.py @@ -0,0 +1,327 @@ +# Copyright © 2010-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 errno +import logging +import re +from os import makedirs, chmod, environ +from os.path import basename, exists, join, dirname +from sys import argv +from dhpython import DEPENDS_SUBSTVARS, PKG_NAME_TPLS, RT_LOCATIONS, RT_TPLS + +log = logging.getLogger('dhpython') +parse_dep = re.compile('''[,\s]* + (?P<name>[^ :]+)(?::any)? + \s* + \(?(?P<version>([>=<]{2,}|=)\s*[^\)]+)?\)? + \s* + (?:\[(?P<arch>[^\]]+)\])? + ''', re.VERBOSE).match + + +def build_options(**options): + """Build an Options object from kw options""" + default_options = { + 'arch': None, + 'package': [], + 'no_package': [], + 'write_log': False, + 'remaining_packages': False, + } + built_options = default_options + built_options.update(options) + return type('Options', (object,), built_options) + + +class DebHelper: + """Reinvents the wheel / some dh functionality (Perl is ugly ;-P)""" + + def __init__(self, options, impl='cpython3'): + self.options = options + self.packages = {} + self.build_depends = {} + self.python_version = None + # Note that each DebHelper instance supports ONE interpreter type only + # it's not possible to mix cpython2, cpython3 and pypy here + self.impl = impl + self.command = { + 'cpython2': 'dh_python2', + 'cpython3': 'dh_python3', + 'pypy': 'dh_pypy', + }[impl] + skip_tpl = set() + for name, tpls in PKG_NAME_TPLS.items(): + if name != impl: + skip_tpl.update(tpls) + skip_tpl = tuple(skip_tpl) + substvar = DEPENDS_SUBSTVARS[impl] + + pkgs = options.package + skip_pkgs = options.no_package + + try: + with open('debian/control', 'r', encoding='utf-8') as fp: + paragraphs = [{}] + field = None + for lineno, line in enumerate(fp, 1): + if line.startswith('#'): + continue + if not line.strip(): + if paragraphs[-1]: + paragraphs.append({}) + field = None + continue + if line[0].isspace(): # Continuation + paragraphs[-1][field] += line.rstrip() + continue + if not ':' in line: + raise Exception( + 'Unable to parse line %i in debian/control: %s' + % (lineno, line)) + field, value = line.split(':', 1) + field = field.lower() + paragraphs[-1][field] = value.strip() + except IOError: + raise Exception('cannot find debian/control file') + + # Trailing new lines? + if not paragraphs[-1]: + paragraphs.pop() + + if len(paragraphs) < 2: + raise Exception('Unable to parse debian/control, found less than ' + '2 paragraphs') + + self.source_name = paragraphs[0]['source'] + if self.impl == 'cpython3' and 'x-python3-version' in paragraphs[0]: + self.python_version = paragraphs[0]['x-python3-version'] + if len(self.python_version.split(',')) > 2: + raise ValueError('too many arguments provided for ' + 'X-Python3-Version: min and max only.') + elif self.impl == 'cpython2': + if 'x-python-version' in paragraphs[0]: + self.python_version = paragraphs[0]['x-python-version'] + elif 'xs-python-version' in paragraphs[0]: + self.python_version = paragraphs[0]['xs-python-version'] + + build_depends = [] + for field in ('build-depends', 'build-depends-indep', + 'build-depends-arch'): + if field in paragraphs[0]: + build_depends.append(paragraphs[0][field]) + build_depends = ', '.join(build_depends) + for dep1 in build_depends.split(','): + for dep2 in dep1.split('|'): + details = parse_dep(dep2) + if details: + details = details.groupdict() + if details['arch']: + architectures = details['arch'].split() + else: + architectures = [None] + for arch in architectures: + self.build_depends.setdefault( + details['name'], {})[arch] = details['version'] + + for paragraph_no, paragraph in enumerate(paragraphs[1:], 2): + if 'package' not in paragraph: + raise Exception('Unable to parse debian/control, paragraph %i ' + 'missing Package field' % paragraph_no) + binary_package = paragraph['package'] + if skip_tpl and binary_package.startswith(skip_tpl): + log.debug('skipping package: %s', binary_package) + continue + if pkgs and binary_package not in pkgs: + continue + if skip_pkgs and binary_package in skip_pkgs: + continue + if (options.remaining_packages and + self.has_acted_on_package(binary_package)): + continue + pkg = { + 'substvars': {}, + 'autoscripts': {}, + 'rtupdates': [], + 'arch': paragraph['architecture'], + } + if (options.arch is False and pkg['arch'] != 'all' or + options.arch is True and pkg['arch'] == 'all'): + # TODO: check also if arch matches current architecture: + continue + + if not binary_package.startswith(PKG_NAME_TPLS[impl]): + # package doesn't have common prefix (python-, python3-, pypy-) + # so lets check if Depends/Recommends contains the + # appropriate substvar + if (substvar not in paragraph.get('depends', '') + and substvar not in paragraph.get('recommends', '')): + log.debug('skipping package %s (missing %s in ' + 'Depends/Recommends)', + binary_package, substvar) + continue + # Operate on binary_package + self.packages[binary_package] = pkg + + fp.close() + log.debug('source=%s, binary packages=%s', self.source_name, + list(self.packages.keys())) + + def has_acted_on_package(self, package): + try: + with open('debian/{}.debhelper.log'.format(package), + encoding='utf-8') as f: + for line in f: + if line.strip() == self.command: + return True + except IOError as e: + if e.errno != errno.ENOENT: + raise + return False + + def addsubstvar(self, package, name, value): + """debhelper's addsubstvar""" + self.packages[package]['substvars'].setdefault(name, []).append(value) + + def autoscript(self, package, when, template, args): + """debhelper's autoscript""" + self.packages[package]['autoscripts'].setdefault(when, {})\ + .setdefault(template, []).append(args) + + def add_rtupdate(self, package, value): + self.packages[package]['rtupdates'].append(value) + + def save_autoscripts(self): + for package, settings in self.packages.items(): + autoscripts = settings.get('autoscripts') + if not autoscripts: + continue + + for when, templates in autoscripts.items(): + fn = "debian/%s.%s.debhelper" % (package, when) + if exists(fn): + with open(fn, 'r', encoding='utf-8') as datafile: + data = datafile.read() + else: + data = '' + + new_data = '' + for tpl_name, args in templates.items(): + for i in args: + # try local one first (useful while testing dh_python3) + fpath = join(dirname(__file__), '..', + "autoscripts/%s" % tpl_name) + if not exists(fpath): + fpath = "/usr/share/debhelper/autoscripts/%s" % tpl_name + with open(fpath, 'r', encoding='utf-8') as tplfile: + tpl = tplfile.read() + if self.options.compile_all and args: + # TODO: should args be checked to contain dir name? + tpl = tpl.replace('-p #PACKAGE#', '') + elif settings['arch'] == 'all': + tpl = tpl.replace('#PACKAGE#', package) + else: + arch = environ['DEB_HOST_ARCH'] + tpl = tpl.replace('#PACKAGE#', '%s:%s' % (package, arch)) + tpl = tpl.replace('#ARGS#', i) + if tpl not in data and tpl not in new_data: + new_data += "\n%s" % tpl + if new_data: + data += '\n# Automatically added by {}'.format(basename(argv[0])) +\ + '{}\n# End automatically added section\n'.format(new_data) + fp = open(fn, 'w', encoding='utf-8') + fp.write(data) + fp.close() + + def save_substvars(self): + for package, settings in self.packages.items(): + substvars = settings.get('substvars') + if not substvars: + continue + fn = "debian/%s.substvars" % package + if exists(fn): + with open(fn, 'r', encoding='utf-8') as datafile: + data = datafile.read() + else: + data = '' + for name, values in substvars.items(): + p = data.find("%s=" % name) + if p > -1: # parse the line and remove it from data + e = data[p:].find('\n') + line = data[p + len("%s=" % name): + p + e if e > -1 else None] + items = [i.strip() for i in line.split(',') if i] + if e > -1 and data[p + e:].strip(): + data = "%s\n%s" % (data[:p], data[p + e:]) + else: + data = data[:p] + else: + items = [] + for j in values: + if j not in items: + items.append(j) + if items: + if data: + data += '\n' + data += "%s=%s\n" % (name, ', '.join(items)) + data = data.replace('\n\n', '\n') + if data: + fp = open(fn, 'w', encoding='utf-8') + fp.write(data) + fp.close() + + def save_rtupdate(self): + for package, settings in self.packages.items(): + pkg_arg = '' if self.options.compile_all else "-p %s" % package + values = settings.get('rtupdates') + if not values: + continue + d = 'debian/{}/{}'.format(package, RT_LOCATIONS[self.impl]) + if not exists(d): + makedirs(d) + fn = "%s/%s.rtupdate" % (d, package) + if exists(fn): + data = open(fn, 'r', encoding='utf-8').read() + else: + data = "#! /bin/sh\nset -e" + for dname, args in values: + cmd = RT_TPLS[self.impl].format(pkg_arg=pkg_arg, + dname=dname, + args=args) + if cmd not in data: + data += "\n%s" % cmd + if data: + fp = open(fn, 'w', encoding='utf-8') + fp.write(data) + fp.close() + chmod(fn, 0o755) + + def save_log(self): + if not self.options.write_log: + return + for package, settings in self.packages.items(): + with open('debian/{}.debhelper.log'.format(package), + 'a', encoding='utf-8') as f: + f.write(self.command + '\n') + + def save(self): + self.save_substvars() + self.save_autoscripts() + self.save_rtupdate() + self.save_log() diff --git a/dhpython/depends.py b/dhpython/depends.py new file mode 100644 index 0000000..e17951c --- /dev/null +++ b/dhpython/depends.py @@ -0,0 +1,281 @@ +# Copyright © 2010-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 +from functools import partial +from os.path import exists, join +from dhpython import PKG_PREFIX_MAP, MINPYCDEP +from dhpython.pydist import parse_pydep, parse_requires_dist, guess_dependency +from dhpython.version import default, supported, VersionRange + +log = logging.getLogger('dhpython') + + +class Dependencies: + """Store relations (dependencies, etc.) between packages.""" + + def __init__(self, package, impl='cpython3', bdep=None): + self.impl = impl + self.package = package + bdep = self.bdep = bdep or {} + self.is_debug_package = dbgpkg = package.endswith('-dbg') + + # TODO: move it to PyPy and CPython{2,3} classes + self.ipkg_vtpl = 'python%s-dbg' if dbgpkg else 'python%s' + if impl == 'cpython3': + self.ipkg_tpl = 'python3-dbg' if dbgpkg else 'python3' + elif impl == 'cpython2': + self.ipkg_tpl = 'python2-dbg' if dbgpkg else 'python2' + elif impl == 'pypy': + self.ipkg_tpl = 'pypy-dbg' if dbgpkg else 'pypy' + self.ipkg_vtpl = 'pypy%s-dbg' if dbgpkg else 'pypy%s' + if impl == 'pypy': + self.ipkg_tpl_ma = self.ipkg_tpl + self.ipkg_vtpl_ma = self.ipkg_vtpl + else: + self.ipkg_tpl_ma = self.ipkg_tpl + ':any' + self.ipkg_vtpl_ma = self.ipkg_vtpl + ':any' + + self.python_dev_in_bd = 'python-dev' in bdep or\ + 'python-all-dev' in bdep or\ + 'python2-dev' in bdep or\ + 'python2-all-dev' in bdep or\ + 'python2.7-dev' in bdep or\ + 'python3-dev' in bdep or\ + 'python3-all-dev' in bdep + + self.depends = set() + self.recommends = [] + self.suggests = [] + self.enhances = [] + self.breaks = [] + self.rtscripts = [] + + def export_to(self, dh): + """Fill in debhelper's substvars.""" + prefix = PKG_PREFIX_MAP.get(self.impl, 'misc') + for i in sorted(self.depends): + dh.addsubstvar(self.package, '{}:Depends'.format(prefix), i) + for i in sorted(self.recommends): + dh.addsubstvar(self.package, '{}:Recommends'.format(prefix), i) + for i in sorted(self.suggests): + dh.addsubstvar(self.package, '{}:Suggests'.format(prefix), i) + for i in sorted(self.enhances): + dh.addsubstvar(self.package, '{}:Enhances'.format(prefix), i) + for i in sorted(self.breaks): + dh.addsubstvar(self.package, '{}:Breaks'.format(prefix), i) + for i in sorted(self.rtscripts): + dh.add_rtupdate(self.package, i) + + def __str__(self): + return "D=%s; R=%s; S=%s; E=%s, B=%s; RT=%s" %\ + (self.depends, self.recommends, self.suggests, + self.enhances, self.breaks, self.rtscripts) + + def depend(self, value): + if value and value not in self.depends: + self.depends.add(value) + + def recommend(self, value): + if value and value not in self.recommends: + self.recommends.append(value) + + def suggest(self, value): + if value and value not in self.suggests: + self.suggests.append(value) + + def enhance(self, value): + if value and value not in self.enhances: + self.enhances.append(value) + + def break_(self, value): + if value and value not in self.breaks: + self.breaks.append(value) + + def rtscript(self, value): + if value not in self.rtscripts: + self.rtscripts.append(value) + + def parse(self, stats, options): + log.debug('generating dependencies for package %s', self.package) + tpl = self.ipkg_tpl + vtpl = self.ipkg_vtpl + tpl_ma = self.ipkg_tpl_ma + vtpl_ma = self.ipkg_vtpl_ma + vrange = options.vrange + + if vrange and any((stats['compile'], stats['public_vers'], + stats['ext_vers'], stats['ext_no_version'], + stats['shebangs'])): + if any((stats['compile'], stats['public_vers'], stats['shebangs'])): + tpl_tmp = tpl_ma + else: + tpl_tmp = tpl + minv = vrange.minver + # note it's an open interval (i.e. do not add 1 here!): + maxv = vrange.maxver + if minv == maxv: + self.depend(vtpl % minv) + minv = maxv = None + if minv: + self.depend("%s (>= %s~)" % (tpl_tmp, minv)) + if maxv: + self.depend("%s (<< %s)" % (tpl_tmp, maxv)) + + if self.impl == 'cpython2' and stats['public_vers']: + # additional Depends to block python package transitions + sorted_vers = sorted(stats['public_vers']) + minv = sorted_vers[0] + maxv = sorted_vers[-1] + if minv <= default(self.impl): + self.depend("%s (>= %s~)" % (tpl_ma, minv)) + if maxv >= default(self.impl): + self.depend("%s (<< %s)" % (tpl_ma, maxv + 1)) + + if self.impl == 'pypy' and stats.get('ext_soabi'): + # TODO: make sure alternative is used only for the same extension names + # ie. for foo.ABI1.so, foo.ABI2.so, bar.ABI3,so, bar.ABI4.so generate: + # pypy-abi-ABI1 | pypy-abi-ABI2, pypy-abi-ABI3 | pypy-abi-ABI4 + self.depend('|'.join(soabi.replace('-', '-abi-') + for soabi in sorted(stats['ext_soabi']))) + + if stats['ext_vers']: + # TODO: what about extensions with stable ABI? + sorted_vers = sorted(stats['ext_vers']) + minv = sorted_vers[0] + maxv = sorted_vers[-1] + #self.depend('|'.join(vtpl % i for i in stats['ext_vers'])) + if minv <= default(self.impl): + self.depend("%s (>= %s~)" % (tpl, minv)) + if maxv >= default(self.impl): + self.depend("%s (<< %s)" % (tpl, maxv + 1)) + + # make sure py{,3}compile binary is available + if stats['compile'] and self.impl in MINPYCDEP: + self.depend(MINPYCDEP[self.impl]) + + for ipreter in stats['shebangs']: + self.depend("%s%s" % (ipreter, '' if self.impl == 'pypy' else ':any')) + + supported_versions = supported(self.impl) + default_version = default(self.impl) + for private_dir, details in stats['private_dirs'].items(): + versions = list(i.version for i in details.get('shebangs', []) if i.version and i.version.minor) + + for v in versions: + if v in supported_versions: + self.depend(vtpl_ma % v) + else: + log.info('dependency on %s (from shebang) ignored' + ' - it\'s not supported anymore', vtpl % v) + # /usr/bin/python{,3} shebang → add python{,3} to Depends + if any(True for i in details.get('shebangs', []) if i.version is None or i.version.minor is None): + self.depend(tpl_ma) + + extensions = False + if self.python_dev_in_bd: + extensions = sorted(details.get('ext_vers', set())) + #self.depend('|'.join(vtpl % i for i in extensions)) + if extensions: + self.depend("%s (>= %s~)" % (tpl, extensions[0])) + self.depend("%s (<< %s)" % (tpl, extensions[-1] + 1)) + elif details.get('ext_no_version'): + # assume unrecognized extension was built for default interpreter version + self.depend("%s (>= %s~)" % (tpl, default_version)) + self.depend("%s (<< %s)" % (tpl, default_version + 1)) + + if details.get('compile'): + if self.impl in MINPYCDEP: + self.depend(MINPYCDEP[self.impl]) + args = '' + if extensions: + args += "-V %s" % VersionRange(minver=extensions[0], maxver=extensions[-1]) + elif len(versions) == 1: # only one version from shebang + #if versions[0] in supported_versions: + args += "-V %s" % versions[0] + # ... otherwise compile with default version + elif details.get('ext_no_version'): + # assume unrecognized extension was built for default interpreter version + args += "-V %s" % default_version + elif vrange: + args += "-V %s" % vrange + if vrange.minver == vrange.maxver: + self.depend(vtpl % vrange.minver) + else: + if vrange.minver: # minimum version specified + self.depend("%s (>= %s~)" % (tpl_ma, vrange.minver)) + if vrange.maxver: # maximum version specified + self.depend("%s (<< %s)" % (tpl_ma, vrange.maxver + 1)) + + for regex in options.regexpr or []: + args += " -X '%s'" % regex.pattern.replace("'", r"'\''") + self.rtscript((private_dir, args)) + + section_options = { + 'depends_sec': options.depends_section, + 'recommends_sec': options.recommends_section, + 'suggests_sec': options.suggests_section, + } + guess_deps = partial(guess_dependency, impl=self.impl, bdep=self.bdep, + accept_upstream_versions=options.accept_upstream_versions) + if options.guess_deps: + for fn in stats['requires.txt']: + # TODO: should options.recommends and options.suggests be + # removed from requires.txt? + deps = parse_pydep(self.impl, fn, bdep=self.bdep, **section_options) + [self.depend(i) for i in deps['depends']] + [self.recommend(i) for i in deps['recommends']] + [self.suggest(i) for i in deps['suggests']] + for fpath in stats['egg-info']: + with open(fpath, 'r', encoding='utf-8') as fp: + for line in fp: + if line.startswith('Requires: '): + req = line[10:].strip() + self.depend(guess_deps(req=req)) + for fpath in stats['dist-info']: + deps = parse_requires_dist(self.impl, fpath, bdep=self.bdep, + **section_options) + [self.depend(i) for i in deps['depends']] + [self.recommend(i) for i in deps['recommends']] + [self.suggest(i) for i in deps['suggests']] + + # add dependencies from --depends + for item in options.depends or []: + self.depend(guess_deps(req=item)) + # add dependencies from --recommends + for item in options.recommends or []: + self.recommend(guess_deps(req=item)) + # add dependencies from --suggests + for item in options.suggests or []: + self.suggest(guess_deps(req=item)) + # add dependencies from --requires + for fn in options.requires or []: + fpath = join('debian', self.package, fn) + if not exists(fpath): + fpath = fn + if not exists(fpath): + log.warn('cannot find requirements file: %s', fn) + continue + deps = parse_pydep(self.impl, fpath, bdep=self.bdep, **section_options) + [self.depend(i) for i in deps['depends']] + [self.recommend(i) for i in deps['recommends']] + [self.suggest(i) for i in deps['suggests']] + + log.debug(self) diff --git a/dhpython/exceptions.py b/dhpython/exceptions.py new file mode 100644 index 0000000..085bb89 --- /dev/null +++ b/dhpython/exceptions.py @@ -0,0 +1,23 @@ +# Copyright © 2022 Stefano Rivera <stefanor@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. + + +class RequiredCommandMissingException(Exception): + pass diff --git a/dhpython/fs.py b/dhpython/fs.py new file mode 100644 index 0000000..445422e --- /dev/null +++ b/dhpython/fs.py @@ -0,0 +1,587 @@ +# Copyright © 2013-2019 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 difflib +import hashlib +import logging +import os +import re +import sys +from filecmp import cmp as cmpfile +from glob import glob +from os.path import (lexists, exists, getsize, isdir, islink, join, realpath, + relpath, split, splitext) +from shutil import rmtree +from stat import ST_MODE, S_IXUSR, S_IXGRP, S_IXOTH +from dhpython import MULTIARCH_DIR_TPL +from dhpython.tools import fix_shebang, clean_egg_name +from dhpython.interpreter import Interpreter + +log = logging.getLogger('dhpython') + + +def fix_locations(package, interpreter, versions, options): + """Move files to the right location.""" + # make a copy since we change version later + interpreter = Interpreter(interpreter) + + for version in versions: + interpreter.version = version + + dstdir = interpreter.sitedir(package) + for srcdir in interpreter.old_sitedirs(package): + if isdir(srcdir): + # TODO: what about relative symlinks? + log.debug('moving files from %s to %s', srcdir, dstdir) + share_files(srcdir, dstdir, interpreter, options) + try: + os.removedirs(srcdir) + except OSError: + pass + + # do the same with debug locations + dstdir = interpreter.sitedir(package, gdb=True) + for srcdir in interpreter.old_sitedirs(package, gdb=True): + if isdir(srcdir): + log.debug('moving files from %s to %s', srcdir, dstdir) + share_files(srcdir, dstdir, interpreter, options) + try: + os.removedirs(srcdir) + except OSError: + pass + + # move files from /usr/include/pythonX.Y/ to …/pythonX.Ym/ + if interpreter.symlinked_include_dir: + srcdir = "debian/%s%s" % (package, interpreter.symlinked_include_dir) + if srcdir and isdir(srcdir): + dstdir = "debian/%s%s" % (package, interpreter.include_dir) + log.debug('moving files from %s to %s', srcdir, dstdir) + share_files(srcdir, dstdir, interpreter, options) + try: + os.removedirs(srcdir) + except OSError: + pass + + +def share_files(srcdir, dstdir, interpreter, options): + """Try to move as many files from srcdir to dstdir as possible.""" + cleanup_actions = [] + for i in os.listdir(srcdir): + fpath1 = join(srcdir, i) + if not lexists(fpath1): # removed in rename_ext + continue + if i.endswith('.pyc'): # f.e. when tests were invoked on installed files + os.remove(fpath1) + continue + if not options.no_ext_rename and splitext(i)[-1] == '.so': + # try to rename extension here as well (in :meth:`scan` info about + # Python version is gone) + version = interpreter.parse_public_dir(srcdir) + if version and version is not True: + fpath1 = Scan.rename_ext(fpath1, interpreter, version) + i = split(fpath1)[-1] + if srcdir.endswith(".dist-info"): + if i in ('COPYING', 'LICENSE') or i.startswith( + ('COPYING.', 'LICENSE.')): + os.remove(fpath1) + cleanup_actions.append((remove_from_RECORD, ([i],))) + continue + elif isdir(fpath1) and i in ('licenses', 'license_files'): + cleanup_actions.append(( + remove_from_RECORD, + ([ + relpath(license, srcdir) + for license in glob(join(srcdir, i, '**')) + ],) + )) + rmtree(fpath1) + continue + fpath2 = join(dstdir, i) + if not isdir(fpath1) and not exists(fpath2): + # do not rename directories here - all .so files have to be renamed first + os.renames(fpath1, fpath2) + continue + if islink(fpath1): + # move symlinks without changing them if they point to the same place + if not exists(fpath2): + os.renames(fpath1, fpath2) + elif realpath(fpath1) == realpath(fpath2): + os.remove(fpath1) + elif isdir(fpath1): + share_files(fpath1, fpath2, interpreter, options) + elif cmpfile(fpath1, fpath2, shallow=False): + os.remove(fpath1) + elif i.endswith(('.abi3.so', '.abi4.so')) and interpreter.parse_public_dir(srcdir): + log.warning('%s differs from previous one, removing anyway (%s)', i, srcdir) + os.remove(fpath1) + elif srcdir.endswith(".dist-info"): + # dist-info file that differs... try merging + if i == "WHEEL": + if merge_WHEEL(fpath1, fpath2): + cleanup_actions.append((fix_merged_RECORD, ())) + os.remove(fpath1) + elif i == "RECORD": + merge_RECORD(fpath1, fpath2) + os.remove(fpath1) + else: + log.warn("No merge driver for dist-info file %s", i) + else: + # The files differed so we cannot collapse them. + log.warn('Paths differ: %s and %s', fpath1, fpath2) + if options.verbose and not i.endswith(('.so', '.a')): + with open(fpath1) as fp1: + fromlines = fp1.readlines() + with open(fpath2) as fp2: + tolines = fp2.readlines() + diff = difflib.unified_diff(fromlines, tolines, fpath1, fpath2) + sys.stderr.writelines(diff) + + for action, args in cleanup_actions: + action(dstdir, *args) + try: + os.removedirs(srcdir) + except OSError: + pass + + +## Functions to merge parts of the .dist-info metadata directory together + +def missing_lines(src, dst): + """Find all the lines in the text file src that are not in dst""" + with open(dst) as fh: + current = {k: None for k in fh.readlines()} + + missing = [] + with open(src) as fh: + for line in fh.readlines(): + if line not in current: + missing.append(line) + + return missing + + +def merge_WHEEL(src, dst): + """Merge the source .dist-info/WHEEL file into the destination + + Note that after editing the WHEEL file, the sha256 included in + the .dist-info/RECORD file will be incorrect and will need fixing + using the fix_merged_RECORD() function. + """ + log.debug("Merging WHEEL file %s into %s", src, dst) + missing = missing_lines(src, dst) + with open(dst, "at") as fh: + for line in missing: + if line.startswith("Tag: "): + fh.write(line) + else: + log.warn("WHEEL merge discarded line %s", line) + + return len(missing) + + +def merge_RECORD(src, dst): + """Merge the source .dist-info/RECORD file into the destination""" + log.debug("Merging RECORD file %s into %s", src, dst) + missing = missing_lines(src, dst) + + with open(dst, "at") as fh: + for line in missing: + fh.write(line) + + return len(missing) + + +def fix_merged_RECORD(distdir): + """Update the checksum for .dist-info/WHEEL in .dist-info/RECORD + + After merging the .dist-info/WHEEL file, the sha256 recorded for it will be + wrong in .dist-info/RECORD, so edit that file to ensure that it is fixed. + The output is sorted for reproducibility. + """ + log.debug("Fixing RECORD file in %s", distdir) + record_path = join(distdir, "RECORD") + wheel_path = join(distdir, "WHEEL") + wheel_dir = split(split(record_path)[0])[1] + wheel_relpath = join(wheel_dir, "WHEEL") + + with open(wheel_path, "rb") as fh: + wheel_sha256 = hashlib.sha256(fh.read()).hexdigest(); + wheel_size = getsize(wheel_path) + + contents = [ + "{name},sha256={sha256sum},{size}\n".format( + name=wheel_relpath, + sha256sum=wheel_sha256, + size=wheel_size, + )] + with open(record_path) as fh: + for line in fh.readlines(): + if not line.startswith(wheel_relpath): + contents.append(line) + # now write out the updated record + with open(record_path, "wt") as fh: + fh.writelines(sorted(contents)) + + +def remove_from_RECORD(distdir, files): + """Remove all specified dist-info files from RECORD""" + log.debug("Removing %r from RECORD in %s", files, distdir) + record = join(distdir, "RECORD") + parent_dir = split(distdir)[1] + names = [join(parent_dir, name) for name in files] + lines = [] + with open(record) as fh: + lines = fh.readlines() + + filtered = [line for line in lines if not line.split(',', 1)[0] in names] + + if lines == filtered: + log.warn("Unable to remove %r from RECORD in %s, not found", + files, distdir) + + with open(record, 'wt') as fh: + fh.writelines(sorted(filtered)) + + +class Scan: + UNWANTED_DIRS = re.compile(r'.*/__pycache__(/.*)?$') + UNWANTED_FILES = re.compile(r'.*\.py[co]$') + + def __init__(self, interpreter, package, dpath=None, options=None): + self.interpreter = interpreter + self.impl = interpreter.impl + + self.package = package + + if not dpath: + self.proot = "debian/%s" % self.package + else: + dpath = dpath.strip('/') + self.proot = join('debian', self.package, dpath) + self.dpath = dpath + del dpath + + self.options = options + self.result = {'requires.txt': set(), + 'egg-info': set(), + 'dist-info': set(), + 'nsp.txt': set(), + 'shebangs': set(), + 'public_vers': set(), + 'private_dirs': {}, + 'compile': False, + 'ext_vers': set(), + 'ext_no_version': set()} + + for root, dirs, file_names in os.walk(self.proot): + if interpreter.should_ignore(root): + del dirs[:] + continue + + self.current_private_dir = self.current_pub_version = None + version = interpreter.parse_public_dir(root) + if version: + self.current_dir_is_public = True + if version is True: + version = None + else: + self.current_pub_version = version + else: + self.current_dir_is_public = False + + if self.current_dir_is_public: + if root.endswith('-packages'): + if version is not None: + self.result['public_vers'].add(version) + for name in dirs: + if name in ('test', 'tests') or name.startswith('.'): + log.debug('removing dist-packages/%s', name) + rmtree(join(root, name)) + dirs.remove(name) + else: + self.current_private_dir = self.check_private_dir(root) + if not self.current_private_dir: + # i.e. not a public dir and not a private dir + if self.is_bin_dir(root): + self.handle_bin_dir(root, file_names) + else: # not a public, private or bin directory + # continue with a subdirectory + continue + + for name in dirs: + dpath = join(root, name) + if self.is_unwanted_dir(dpath): + rmtree(dpath) + dirs.remove(name) + continue + + if self.is_dist_dir(root): + self.handle_dist_dir(root, file_names) + continue + + if self.is_egg_dir(root): + self.handle_egg_dir(root, file_names) + continue + + # check files + for fn in sorted(file_names): + # sorted() to make sure .so files are handled before .so.foo + fpath = join(root, fn) + + if self.is_unwanted_file(fpath): + log.debug('removing unwanted: %s', fpath) + os.remove(fpath) + continue + + if self.is_egg_file(fpath): + self.handle_egg_file(fpath) + continue + + if not exists(fpath): + # possibly removed while handling .so symlinks + if islink(fpath) and '.so.' in split(fpath)[-1]: + # dangling symlink to (now removed/renamed) .so file + # which wasn't removed yet (see test203's quux.so.0) + log.info('removing dangling symlink: %s', fpath) + os.remove(fpath) + continue + + fext = splitext(fn)[-1][1:] + if fext == 'so': + if not self.options.no_ext_rename: + fpath = self.rename_ext(fpath, interpreter, version) + ver = self.handle_ext(fpath) + ver = ver or version + if ver: + self.current_result.setdefault('ext_vers', set()).add(ver) + else: + self.current_result.setdefault('ext_no_version', set()).add(fpath) + + if self.current_private_dir: + if exists(fpath) and fext != 'so': + mode = os.stat(fpath)[ST_MODE] + if mode & S_IXUSR or mode & S_IXGRP or mode & S_IXOTH: + if (options.no_shebang_rewrite or + fix_shebang(fpath, self.options.shebang)) and \ + not self.options.ignore_shebangs: + try: + res = Interpreter.from_file(fpath) + except Exception as e: + log.debug('cannot parse shebang %s: %s', fpath, e) + else: + self.current_result.setdefault('shebangs', set()).add(res) + + if fext == 'py' and self.handle_public_module(fpath) is not False: + self.current_result['compile'] = True + + if not dirs and not self.current_private_dir: + try: + os.removedirs(root) + except OSError: + pass + + log.debug("package %s details = %s", package, self.result) + + @property + def current_result(self): + if self.current_private_dir: + return self.result['private_dirs'].setdefault(self.current_private_dir, {}) + return self.result + + def is_unwanted_dir(self, dpath): + return self.__class__.UNWANTED_DIRS.match(dpath) + + def is_unwanted_file(self, fpath): + if self.__class__.UNWANTED_FILES.match(fpath): + return True + if self.current_dir_is_public and self.is_dbg_package\ + and self.options.clean_dbg_pkg\ + and splitext(fpath)[-1][1:] not in ('so', 'h'): + return True + + @property + def private_dirs_to_check(self): + if self.dpath: + # scan private directory *only* + return [self.dpath] + + if self.dpath is False: + result = [] + else: + result = [i % self.package for i in ( + 'usr/lib/%s', + 'usr/lib/games/%s', + 'usr/share/%s', + 'usr/share/games/%s')] + return result + + @property + def is_dbg_package(self): + #return self.interpreter.debug + return self.package.endswith('-dbg') + + def check_private_dir(self, dpath): + """Return private dir's root if it's a private dir.""" + for i in self.private_dirs_to_check: + if dpath.startswith(join('debian', self.package, i)): + return '/' + i + + @staticmethod + def rename_ext(fpath, interpreter, current_pub_version=None): + """Add multiarch triplet, etc. Return new name. + + This method is invoked for all .so files in public or private directories. + """ + # current_pub_version - version parsed from dist-packages (True if unversioned) + # i.e. if it's not None - it's a public dist-packages directory + + path, fname = fpath.rsplit('/', 1) + if current_pub_version is not None and islink(fpath): + # replace symlinks with extensions in dist-packages directory + dstfpath = fpath + links = set() + while islink(dstfpath): + links.add(dstfpath) + dstfpath = join(path, os.readlink(dstfpath)) + if exists(dstfpath) and '.so.' in split(dstfpath)[-1]: + # rename .so.$FOO symlinks, remove other ones + for lpath in links: + log.info('removing symlink: %s', lpath) + os.remove(lpath) + log.info('renaming %s to %s', dstfpath, fname) + os.rename(dstfpath, fpath) + + if MULTIARCH_DIR_TPL.match(fpath): + # ignore /lib/i386-linux-gnu/, /usr/lib/x86_64-kfreebsd-gnu/, etc. + return fpath + + new_fn = interpreter.check_extname(fname, current_pub_version) + if new_fn: + # TODO: what about symlinks pointing to this file + new_fpath = join(path, new_fn) + if exists(new_fpath): + log.warn('destination file exist, ' + 'cannot rename %s to %s', fname, new_fn) + else: + log.info('renaming %s to %s', fname, new_fn) + os.rename(fpath, new_fpath) + return new_fpath + return fpath + + def handle_ext(self, fpath): + """Handle .so file, return its version if detected.""" + + def handle_public_module(self, fpath): + pass + + def is_bin_dir(self, dpath): + """Check if dir is one from PATH ones.""" + # dname = debian/packagename/usr/games + spath = dpath.strip('/').split('/', 4) + if len(spath) > 4: + return False # assume bin directories don't have subdirectories + if dpath.endswith(('/sbin', '/bin', '/usr/games')): + # /(s)bin or /usr/(s)bin or /usr/games + return True + + def handle_bin_dir(self, dpath, file_names): + if self.options.no_shebang_rewrite or self.options.ignore_shebangs: + return + for fn in file_names: + fpath = join(dpath, fn) + if fix_shebang(fpath, self.options.shebang): + try: + res = Interpreter.from_file(fpath) + except Exception as e: + log.debug('cannot parse shebang %s: %s', fpath, e) + else: + self.result['shebangs'].add(res) + + def is_egg_dir(self, dname): + """Check if given directory contains egg-info.""" + return dname.endswith('.egg-info') + + def handle_egg_dir(self, dpath, file_names): + path, dname = dpath.rsplit('/', 1) + if self.is_dbg_package and self.options.clean_dbg_pkg: + rmtree(dpath) + return + + clean_name = clean_egg_name(dname) + if clean_name != dname: + if exists(join(path, clean_name)): + log.info('removing %s (%s is already available)', dname, clean_name) + rmtree(dpath) + return + else: + log.info('renaming %s to %s', dname, clean_name) + os.rename(dpath, join(path, clean_name)) + dname = clean_name + dpath = join(path, dname) + if file_names: + if 'requires.txt' in file_names: + self.result['requires.txt'].add(join(dpath, 'requires.txt')) + if 'namespace_packages.txt' in file_names: + self.result['nsp.txt'].add(join(dpath, 'namespace_packages.txt')) + if 'SOURCES.txt' in file_names: + os.remove(join(dpath, 'SOURCES.txt')) + file_names.remove('SOURCES.txt') + + def is_egg_file(self, fpath): + """Check if given file contains egg-info.""" + return fpath.endswith('.egg-info') + + def handle_egg_file(self, fpath): + root, name = fpath.rsplit('/', 1) + clean_name = clean_egg_name(name) + if clean_name != name: + if exists(join(root, clean_name)): + log.info('removing %s (%s is already available)', + name, clean_name) + os.remove(fpath) + else: + log.info('renaming %s to %s', name, clean_name) + os.rename(fpath, join(root, clean_name)) + self.result['egg-info'].add(join(root, clean_name)) + + def is_dist_dir(self, dname): + """Check if given directory contains dist-info.""" + return dname.endswith('.dist-info') + + def handle_dist_dir(self, dpath, file_names): + path, dname = dpath.rsplit('/', 1) + if self.is_dbg_package and self.options.clean_dbg_pkg: + rmtree(dpath) + return + + if file_names: + if 'METADATA' in file_names: + self.result['dist-info'].add(join(dpath, 'METADATA')) + + def cleanup(self): + if self.is_dbg_package and self.options.clean_dbg_pkg: + # remove empty directories in -dbg packages + proot = self.proot + '/usr/lib' + for root, dirs, file_names in os.walk(proot, topdown=False): + if '-packages/' in root and not file_names: + try: + os.removedirs(root) + except Exception: + pass 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 diff --git a/dhpython/markers.py b/dhpython/markers.py new file mode 100644 index 0000000..a0a3e55 --- /dev/null +++ b/dhpython/markers.py @@ -0,0 +1,70 @@ +# Copyright © 2022 Stefano Rivera <stefanor@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. + +""" +Handle Environment Markers +https://www.python.org/dev/peps/pep-0508/#environment-markers + +TODO: Ideally replace with the packaging library, but the API is currently +private: https://github.com/pypa/packaging/issues/496 +""" + +import re + + +SIMPLE_ENV_MARKER_RE = re.compile(r''' + (?P<marker>[a-z_]+) + \s* + (?P<op><=?|>=?|[=!~]=|===) + \s* + (?P<quote>['"]) + (?P<value>.*) # Could contain additional markers + (?P=quote) + ''', re.VERBOSE) +COMPLEX_ENV_MARKER_RE = re.compile(r''' + (?:\s|\)) + (?:and|or) + (?:\s|\() + ''', re.VERBOSE) + + +class ComplexEnvironmentMarker(Exception): + pass + + +def parse_environment_marker(marker): + """Parse a simple marker of <= 1 environment restriction""" + marker = marker.strip() + if marker.startswith('(') and marker.endswith(')'): + marker = marker[1:-1].strip() + + m = COMPLEX_ENV_MARKER_RE.search(marker) + if m: + raise ComplexEnvironmentMarker() + + m = SIMPLE_ENV_MARKER_RE.match(marker) + if not m: + raise ComplexEnvironmentMarker() + + return ( + m.group('marker'), + m.group('op'), + m.group('value'), + ) diff --git a/dhpython/option.py b/dhpython/option.py new file mode 100644 index 0000000..7d70dbe --- /dev/null +++ b/dhpython/option.py @@ -0,0 +1,30 @@ +# -*- coding: UTF-8 -*- +# Copyright © 2010-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 re + + +def compiled_regex(string): + """argparse regex type""" + try: + return re.compile(string) + except re.error: + raise ValueError("regular expression is not valid") diff --git a/dhpython/pydist.py b/dhpython/pydist.py new file mode 100644 index 0000000..015b5f2 --- /dev/null +++ b/dhpython/pydist.py @@ -0,0 +1,692 @@ +# Copyright © 2010-2020 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 email +import logging +import platform +import os +import re +from functools import partial +from os.path import exists, isdir, join +from subprocess import PIPE, Popen + +if __name__ == '__main__': + import sys + sys.path.append(os.path.abspath(join(os.path.dirname(__file__), '..'))) + +from dhpython import PKG_PREFIX_MAP, PUBLIC_DIR_RE,\ + PYDIST_DIRS, PYDIST_OVERRIDES_FNAMES, PYDIST_DPKG_SEARCH_TPLS +from dhpython.markers import ComplexEnvironmentMarker, parse_environment_marker +from dhpython.tools import memoize +from dhpython.version import get_requested_versions, Version + +log = logging.getLogger('dhpython') + +PYDIST_RE = re.compile(r""" + (?P<name>[A-Za-z][A-Za-z0-9_.-]*) # Python distribution name + \s* + (?P<vrange>(?:-?\d\.\d+(?:-(?:\d\.\d+)?)?)?) # version range + \s* + (?P<dependency>(?:[a-z][^;]*)?) # Debian dependency + (?: # optional upstream version -> Debian version translator + ;\s* + (?P<standard>PEP386)? # PEP-386 mode + \s* + (?P<rules>(?:s|tr|y).*)? # translator rules + )? + """, re.VERBOSE) +REQUIRES_RE = re.compile(r''' + (?P<name>[A-Za-z][A-Za-z0-9_.-]*) # Python distribution name + \s* + (?P<enabled_extras>(?:\[[^\]]*\])?) # ignored for now + \s* + \(? # optional parenthesis + (?: # optional minimum/maximum version + (?P<operator><=?|>=?|==|!=|~=) + \s* + (?P<version>(\w|[-.*])+) + (?: # optional interval minimum/maximum version + \s* + , + \s* + (?P<operator2><=?|>=?|==|!=) + \s* + (?P<version2>(\w|[-.])+) + )? + )? + \)? # optional closing parenthesis + \s* + (?:; # optional environment markers + (?P<environment_marker>.+) + )? + ''', re.VERBOSE) +EXTRA_RE = re.compile(r''' + ; + \s* + extra + \s* + == + \s* + (?P<quote>['"]) + (?P<section>[a-zA-Z0-9-_.]+) + (?P=quote) + ''', re.VERBOSE) +REQ_SECTIONS_RE = re.compile(r''' + ^ + \[ + (?P<section>[a-zA-Z0-9-_.]+)? + \s* + (?:: + (?P<environment_marker>.+) + )? + \] + \s* + $ + ''', re.VERBOSE) +DEB_VERS_OPS = { + '==': '=', + '<': '<<', + '>': '>>', + '~=': '>=', +} + + +def validate(fpath): + """Check if pydist file looks good.""" + with open(fpath, encoding='utf-8') as fp: + for line in fp: + line = line.strip() + if line.startswith('#') or not line: + continue + if not PYDIST_RE.match(line): + log.error('invalid pydist data in file %s: %s', + fpath.rsplit('/', 1)[-1], line) + return False + return True + + +@memoize +def load(impl): + """Load information about installed Python distributions. + + :param impl: interpreter implementation, f.e. cpython2, cpython3, pypy + :type impl: str + """ + fname = PYDIST_OVERRIDES_FNAMES.get(impl) + if exists(fname): + to_check = [fname] # first one! + else: + to_check = [] + + dname = PYDIST_DIRS.get(impl) + if isdir(dname): + to_check.extend(join(dname, i) for i in os.listdir(dname)) + + fbdir = os.environ.get('DH_PYTHON_DIST', '/usr/share/dh-python/dist/') + fbname = join(fbdir, '{}_fallback'.format(impl)) + if exists(fbname): # fall back generated at dh-python build time + to_check.append(fbname) # last one! + + result = {} + for fpath in to_check: + with open(fpath, encoding='utf-8') as fp: + for line in fp: + line = line.strip() + if line.startswith('#') or not line: + continue + dist = PYDIST_RE.search(line) + if not dist: + raise Exception('invalid pydist line: %s (in %s)' % (line, fpath)) + dist = dist.groupdict() + name = safe_name(dist['name']) + dist['versions'] = get_requested_versions(impl, dist['vrange']) + dist['dependency'] = dist['dependency'].strip() + if dist['rules']: + dist['rules'] = dist['rules'].split(';') + else: + dist['rules'] = [] + result.setdefault(name, []).append(dist) + return result + + +def guess_dependency(impl, req, version=None, bdep=None, + accept_upstream_versions=False): + bdep = bdep or {} + log.debug('trying to find dependency for %s (python=%s)', + req, version) + if isinstance(version, str): + version = Version(version) + + # some upstreams have weird ideas for distribution name... + name, rest = re.compile('([^!><=~ \(\)\[;]+)(.*)').match(req).groups() + # TODO: check stdlib and dist-packaged for name.py and name.so files + req = safe_name(name) + rest + + data = load(impl) + req_d = REQUIRES_RE.match(req) + if not req_d: + log.info('please ask dh_python3 author to fix REQUIRES_RE ' + 'or your upstream author to fix requires.txt') + raise Exception('requirement is not valid: %s' % req) + req_d = req_d.groupdict() + + env_marker_alts = '' + if req_d['environment_marker']: + action = check_environment_marker_restrictions( + req, + req_d['environment_marker'], + impl) + if action is False: + return + elif action is True: + pass + else: + env_marker_alts = ' ' + action + + name = req_d['name'] + details = data.get(name.lower()) + if details: + log.debug("dependency: module seems to be installed") + for item in details: + if version and version not in item.get('versions', version): + # rule doesn't match version, try next one + continue + if not item['dependency']: + log.debug("dependency: requirement ignored") + return # this requirement should be ignored + if item['dependency'].endswith(')'): + # no need to translate versions if version is hardcoded in + # Debian dependency + log.debug("dependency: requirement already has hardcoded version") + return item['dependency'] + env_marker_alts + if req_d['operator'] == '==' and req_d['version'].endswith('*'): + # Translate "== 1.*" to "~= 1.0" + req_d['operator'] = '~=' + req_d['version'] = req_d['version'].replace('*', '0') + log.debug("dependency: translated wildcard version to semver limit") + if req_d['version'] and (item['standard'] or item['rules']) and\ + req_d['operator'] not in (None, '!='): + o = _translate_op(req_d['operator']) + v = _translate(req_d['version'], item['rules'], item['standard']) + d = "%s (%s %s)%s" % ( + item['dependency'], o, v, env_marker_alts) + if req_d['version2'] and req_d['operator2'] not in (None,'!='): + o2 = _translate_op(req_d['operator2']) + v2 = _translate(req_d['version2'], item['rules'], item['standard']) + d += ", %s (%s %s)%s" % ( + item['dependency'], o2, v2, env_marker_alts) + elif req_d['operator'] == '~=': + o2 = '<<' + v2 = _translate(_max_compatible(req_d['version']), item['rules'], item['standard']) + d += ", %s (%s %s)%s" % ( + item['dependency'], o2, v2, env_marker_alts) + log.debug("dependency: constructed version") + return d + elif accept_upstream_versions and req_d['version'] and \ + req_d['operator'] not in (None,'!='): + o = _translate_op(req_d['operator']) + d = "%s (%s %s)%s" % ( + item['dependency'], o, req_d['version'], env_marker_alts) + if req_d['version2'] and req_d['operator2'] not in (None,'!='): + o2 = _translate_op(req_d['operator2']) + d += ", %s (%s %s)%s" % ( + item['dependency'], o2, req_d['version2'], + env_marker_alts) + elif req_d['operator'] == '~=': + o2 = '<<' + d += ", %s (%s %s)%s" % ( + item['dependency'], o2, + _max_compatible(req_d['version']), env_marker_alts) + log.debug("dependency: constructed upstream version") + return d + else: + if item['dependency'] in bdep: + if None in bdep[item['dependency']] and bdep[item['dependency']][None]: + log.debug("dependency: included in build-deps with limits ") + return "{} ({}){}".format( + item['dependency'], bdep[item['dependency']][None], + env_marker_alts) + # if arch in bdep[item['dependency']]: + # TODO: handle architecture specific dependencies from build depends + # (current architecture is needed here) + log.debug("dependency: included in build-deps") + return item['dependency'] + env_marker_alts + + # search for Egg metadata file or directory (using dpkg -S) + dpkg_query_tpl, regex_filter = PYDIST_DPKG_SEARCH_TPLS[impl] + dpkg_query = dpkg_query_tpl.format(ci_regexp(safe_name(name))) + + log.debug("invoking dpkg -S %s", dpkg_query) + process = Popen(('/usr/bin/dpkg', '-S', dpkg_query), + stdout=PIPE, stderr=PIPE) + stdout, stderr = process.communicate() + if process.returncode == 0: + result = set() + stdout = str(stdout, 'utf-8') + for line in stdout.split('\n'): + if not line.strip(): + continue + pkg, path = line.split(':', 1) + if regex_filter and not re.search(regex_filter, path): + continue + result.add(pkg) + if len(result) > 1: + log.error('more than one package name found for %s dist', name) + elif not result: + log.debug('dpkg -S did not find package for %s', name) + else: + log.debug('dependency: found a result with dpkg -S') + return result.pop() + env_marker_alts + else: + log.debug('dpkg -S did not find package for %s: %s', name, stderr) + + pname = sensible_pname(impl, name) + log.info('Cannot find package that provides %s. ' + 'Please add package that provides it to Build-Depends or ' + 'add "%s %s" line to %s or add proper ' + 'dependency to Depends by hand and ignore this info.', + name, safe_name(name), pname, PYDIST_OVERRIDES_FNAMES[impl]) + # return pname + + +def check_environment_marker_restrictions(req, marker_str, impl): + """Check wither we should include or skip a dependency based on its + environment markers. + + Returns: True - to keep a dependency + False - to skip it + str - to append "| foo" to generated dependencies + """ + if impl != 'cpython3': + log.info('Ignoring environment markers for non-Python 3.x: %s', req) + return False + + try: + marker, op, value = parse_environment_marker(marker_str) + except ComplexEnvironmentMarker: + log.info('Ignoring complex environment marker: %s', req) + return False + + # TODO: Use dynamic values when building arch-dependent + # binaries, otherwise static values + # TODO: Hurd values? + supported_values = { + 'implementation_name': ('cpython', 'pypy'), + 'os_name': ('posix',), + 'platform_system': ('GNU/kFreeBSD', 'Linux'), + 'platform_machine': (platform.machine(),), + 'platform_python_implementation': ('CPython', 'PyPy'), + 'sys_platform': ( + 'gnukfreebsd8', 'gnukfreebsd9', 'gnukfreebsd10', + 'gnukfreebsd11', 'gnukfreebsd12', 'gnukfreebsd13', + 'linux'), + } + if marker in supported_values: + sv = supported_values[marker] + if op in ('==', '!='): + if ((op == '==' and value not in sv) + or (op == '!=' and value in sv)): + log.debug('Skipping requirement (%s != %s): %s', + value, sv, req) + return False + else: + log.info( + 'Skipping requirement with unhandled environment marker ' + 'comparison: %s', req) + return False + + elif marker in ('python_version', 'python_full_version', + 'implementation_version'): + # TODO: Replace with full PEP-440 parser + env_ver = value + split_ver = value.split('.') + if marker == 'python_version': + version_parts = 2 + elif marker == 'python_full_version': + version_parts = 3 + else: + version_parts = len(split_ver) + + if '*' in env_ver: + if split_ver.index('*') != len(split_ver) -1: + log.info('Skipping requirement with intermediate wildcard: %s', + req) + return False + split_ver.pop() + env_ver = '.'.join(split_ver) + if op == '==': + if marker == 'python_full_version': + marker = 'python_version' + version_parts = 2 + else: + op == '=~' + elif op == '!=': + if marker == 'python_full_version': + marker = 'python_version' + version_parts = 2 + else: + log.info('Ignoring wildcard != requirement, not ' + 'representable in Debian: %s', req) + return True + else: + log.info('Skipping requirement with %s on a wildcard: %s', + op, req) + return False + + int_ver = [] + for ver_part in split_ver: + if ver_part.isdigit(): + int_ver.append(int(ver_part)) + else: + env_ver = '.'.join(str(x) for x in int_ver) + log.info('Truncating unparseable version %s to %s in %s', + value, env_ver, req) + break + + if len(int_ver) < version_parts: + int_ver.append(0) + env_ver += '.0' + next_ver = int_ver.copy() + next_ver[version_parts - 1] += 1 + next_ver = '.'.join(str(x) for x in next_ver) + prev_ver = int_ver.copy() + prev_ver[version_parts - 1] -= 1 + prev_ver = '.'.join(str(x) for x in prev_ver) + + if op == '<': + if int_ver <= [3, 0, 0]: + return False + return '| python3 (>> {})'.format(env_ver) + elif op == '<=': + return '| python3 (>> {})'.format(next_ver) + elif op == '>=': + if int_ver < [3, 0, 0]: + return True + return '| python3 (<< {})'.format(env_ver) + elif op == '>': + if int_ver < [3, 0, 0]: + return True + return '| python3 (<< {})'.format(next_ver) + elif op in ('==', '==='): + # === is arbitrary equality (PEP 440) + if marker == 'python_version' or op == '==': + return '| python3 (<< {}) | python3 (>> {})'.format( + env_ver, next_ver) + else: + log.info( + 'Skipping requirement with %s environment marker, cannot ' + 'model in Debian deps: %s', op, req) + return False + elif op == '~=': # Compatible equality (PEP 440) + ceq_next_ver = int_ver[:2] + ceq_next_ver[1] += 1 + ceq_next_ver = '.'.join(str(x) for x in ceq_next_ver) + return '| python3 (<< {}) | python3 (>> {})'.format( + env_ver, ceq_next_ver) + elif op == '!=': + log.info('Ignoring != comparison in environment marker, cannot ' + 'model in Debian deps: %s', req) + return True + + elif marker == 'extra': + # Handled in section logic of parse_requires_dist() + return True + else: + log.info('Skipping requirement with unknown environment marker: %s', + marker) + return False + return True + + +def parse_pydep(impl, fname, bdep=None, options=None, + depends_sec=None, recommends_sec=None, suggests_sec=None): + depends_sec = depends_sec or [] + recommends_sec = recommends_sec or [] + suggests_sec = suggests_sec or [] + + public_dir = PUBLIC_DIR_RE[impl].match(fname) + ver = None + if public_dir and public_dir.groups() and len(public_dir.group(1)) != 1: + ver = public_dir.group(1) + + guess_deps = partial(guess_dependency, impl=impl, version=ver, bdep=bdep, + accept_upstream_versions=getattr( + options, 'accept_upstream_versions', False)) + + result = {'depends': [], 'recommends': [], 'suggests': []} + modified = section = False + env_action = True + processed = [] + with open(fname, 'r', encoding='utf-8') as fp: + for line in fp: + line = line.strip() + if not line or line.startswith('#'): + processed.append(line) + continue + if line.startswith('['): + m = REQ_SECTIONS_RE.match(line) + if not m: + log.info('Skipping section %s, unable to parse header', + line) + processed.append(line) + section = object() + continue + section = m.group('section') + env_action = True + if m.group('environment_marker'): + env_action = check_environment_marker_restrictions( + line, + m.group('environment_marker'), + impl) + processed.append(line) + continue + if section: + if section in depends_sec: + result_key = 'depends' + elif section in recommends_sec: + result_key = 'recommends' + elif section in suggests_sec: + result_key = 'suggests' + else: + processed.append(line) + continue + else: + result_key = 'depends' + + dependency = None + if env_action: + dependency = guess_deps(req=line) + if dependency and isinstance(env_action, str): + dependency = ', '.join( + part.strip() + ' ' + env_action + for part in dependency.split(',')) + + if dependency: + result[result_key].append(dependency) + modified = True + else: + processed.append(line) + if modified and public_dir: + with open(fname, 'w', encoding='utf-8') as fp: + fp.writelines(i + '\n' for i in processed) + return result + + +def parse_requires_dist(impl, fname, bdep=None, options=None, depends_sec=None, + recommends_sec=None, suggests_sec=None): + """Extract dependencies from a dist-info/METADATA file""" + depends_sec = depends_sec or [] + recommends_sec = recommends_sec or [] + suggests_sec = suggests_sec or [] + + public_dir = PUBLIC_DIR_RE[impl].match(fname) + ver = None + if public_dir and public_dir.groups() and len(public_dir.group(1)) != 1: + ver = public_dir.group(1) + + guess_deps = partial(guess_dependency, impl=impl, version=ver, bdep=bdep, + accept_upstream_versions=getattr( + options, 'accept_upstream_versions', False)) + result = {'depends': [], 'recommends': [], 'suggests': []} + section = None + with open(fname, 'r', encoding='utf-8') as fp: + metadata = email.message_from_string(fp.read()) + requires = metadata.get_all('Requires-Dist', []) + for req in requires: + m = EXTRA_RE.search(req) + result_key = 'depends' + if m: + section = m.group('section') + if section: + if section in depends_sec: + result_key = 'depends' + elif section in recommends_sec: + result_key = 'recommends' + elif section in suggests_sec: + result_key = 'suggests' + else: + continue + dependency = guess_deps(req=req) + if dependency: + result[result_key].append(dependency) + return result + + +def safe_name(name): + """Emulate distribute's safe_name.""" + return re.compile('[^A-Za-z0-9.]+').sub('_', name).lower() + + +def sensible_pname(impl, egg_name): + """Guess Debian package name from Egg name.""" + egg_name = safe_name(egg_name).replace('_', '-') + if egg_name.startswith('python-'): + egg_name = egg_name[7:] + return '{}-{}'.format(PKG_PREFIX_MAP[impl], egg_name.lower()) + + +def ci_regexp(name): + """Return case insensitive dpkg -S regexp.""" + return ''.join("[%s%s]" % (i.upper(), i) if i.isalpha() else i for i in name.lower()) + + +PRE_VER_RE = re.compile(r'[-.]?(alpha|beta|rc|dev|a|b|c)') +GROUP_RE = re.compile(r'\$(\d+)') + + +def _pl2py(pattern): + """Convert Perl RE patterns used in uscan to Python's + + >>> print(_pl2py('foo$3')) + foo\g<3> + """ + return GROUP_RE.sub(r'\\g<\1>', pattern) + + +def _max_compatible(version): + """Return the maximum version compatible with `version` in PEP440 terms, + used by ~= requires version specifiers. + + https://www.python.org/dev/peps/pep-0440/#compatible-release + + >>> _max_compatible('2.2') + '3' + >>> _max_compatible('1.4.5') + '1.5' + >>> _max_compatible('1.3.alpha4') + '2' + >>> _max_compatible('2.1.3.post5') + '2.2' + + """ + v = Version(version) + v.serial = None + v.releaselevel = None + if v.micro is not None: + v.micro = None + return str(v + 1) + v.minor = None + return str(v + 1) + + +def _translate(version, rules, standard): + """Translate Python version into Debian one. + + >>> _translate('1.C2betac', ['s/c//gi'], None) + '1.2beta' + >>> _translate('5-fooa1.2beta3-fooD', + ... ['s/^/1:/', 's/-foo//g', 's:([A-Z]):+$1:'], 'PEP386') + '1:5~a1.2~beta3+D' + >>> _translate('x.y.x.z', ['tr/xy/ab/', 'y,z,Z,'], None) + 'a.b.a.Z' + """ + for rule in rules: + # uscan supports s, tr and y operations + if rule.startswith(('tr', 'y')): + # Note: no support for escaped separator in the pattern + pos = 1 if rule.startswith('y') else 2 + tmp = rule[pos + 1:].split(rule[pos]) + version = version.translate(str.maketrans(tmp[0], tmp[1])) + elif rule.startswith('s'): + # uscan supports: g, u and x flags + tmp = rule[2:].split(rule[1]) + pattern = re.compile(tmp[0]) + count = 1 + if tmp[2:]: + flags = tmp[2] + if 'g' in flags: + count = 0 + if 'i' in flags: + pattern = re.compile(tmp[0], re.I) + version = pattern.sub(_pl2py(tmp[1]), version, count) + else: + log.warn('unknown rule ignored: %s', rule) + if standard == 'PEP386': + version = PRE_VER_RE.sub(r'~\g<1>', version) + return version + + +def _translate_op(operator): + """Translate Python version operator into Debian one. + + >>> _translate_op('==') + '=' + >>> _translate_op('<') + '<<' + >>> _translate_op('<=') + '<=' + """ + return DEB_VERS_OPS.get(operator, operator) + + +if __name__ == '__main__': + impl = os.environ.get('IMPL', 'cpython3') + for i in sys.argv[1:]: + if os.path.isfile(i): + try: + print(', '.join(parse_pydep(impl, i)['depends'])) + except Exception as err: + log.error('%s: cannot guess (%s)', i, err) + else: + try: + print(guess_dependency(impl, i) or '') + except Exception as err: + log.error('%s: cannot guess (%s)', i, err) diff --git a/dhpython/tools.py b/dhpython/tools.py new file mode 100644 index 0000000..512f944 --- /dev/null +++ b/dhpython/tools.py @@ -0,0 +1,340 @@ +# -*- coding: UTF-8 -*- +# Copyright © 2010-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 +import locale +from datetime import datetime +from glob import glob +from pickle import dumps +from shutil import rmtree +from os.path import exists, getsize, isdir, islink, join, split +from subprocess import Popen, PIPE + +log = logging.getLogger('dhpython') +EGGnPTH_RE = re.compile(r'(.*?)(-py\d\.\d(?:-[^.]*)?)?(\.egg-info|\.pth)$') +SHAREDLIB_RE = re.compile(r'NEEDED.*libpython(\d\.\d)') + + +def relpath(target, link): + """Return relative path. + + >>> relpath('/usr/share/python-foo/foo.py', '/usr/bin/foo', ) + '../share/python-foo/foo.py' + """ + t = target.split('/') + l = link.split('/') + while l and l[0] == t[0]: + del l[0], t[0] + return '/'.join(['..'] * (len(l) - 1) + t) + + +def relative_symlink(target, link): + """Create relative symlink.""" + return os.symlink(relpath(target, link), link) + + +def move_file(fpath, dstdir): + """Move file to dstdir. Works with symlinks (including relative ones).""" + if isdir(fpath): + dname = split(fpath)[-1] + for fn in os.listdir(fpath): + move_file(join(fpath, fn), join(dstdir, dname)) + + if islink(fpath): + dstpath = join(dstdir, split(fpath)[-1]) + relative_symlink(os.readlink(fpath), dstpath) + os.remove(fpath) + else: + os.rename(fpath, dstdir) + + +def move_matching_files(src, dst, pattern, sub=None, repl=''): + """Move files (preserving path) that match given pattern. + + move_matching_files('foo/bar/', 'foo/baz/', 'spam/.*\.so$') + will move foo/bar/a/b/c/spam/file.so to foo/baz/a/b/c/spam/file.so + + :param sub: regular expression for path part that will be replaced with `repl` + :param repl: replacement for `sub` + """ + match = re.compile(pattern).search + if sub: + sub = re.compile(sub).sub + repl = repl or '' + for root, dirs, filenames in os.walk(src): + for fn in filenames: + spath = join(root, fn) + if match(spath): + if sub is not None: + spath = sub(repl, spath) + dpath = join(dst, relpath(spath, src)) + os.renames(spath, dpath) + + +def fix_shebang(fpath, replacement=None): + """Normalize file's shebang. + + :param replacement: new shebang command (path to interpreter and options) + """ + try: + interpreter = Interpreter.from_file(fpath) + except Exception as err: + log.debug('fix_shebang (%s): %s', fpath, err) + return None + + if not replacement and interpreter.version == '2': + # we'll drop /usr/bin/python symlink from python package at some point + replacement = '/usr/bin/python2' + if interpreter.debug: + replacement += '-dbg' + elif not replacement and interpreter.path != '/usr/bin/': # f.e. /usr/local/* or */bin/env + interpreter.path = '/usr/bin' + replacement = repr(interpreter) + if replacement: + log.info('replacing shebang in %s', fpath) + try: + with open(fpath, 'rb') as fp: + fcontent = fp.readlines() + except IOError: + log.error('cannot open %s', fpath) + return False + # do not catch IOError here, the file is zeroed at this stage so it's + # better to fail + with open(fpath, 'wb') as fp: + fp.write(("#! %s\n" % replacement).encode('utf-8')) + fp.writelines(fcontent[1:]) + return True + + +def so2pyver(fpath): + """Return libpython version file is linked to or None. + + :rtype: tuple + :returns: Python version + """ + + cmd = "readelf -Wd '%s'" % fpath + process = Popen(cmd, stdout=PIPE, shell=True) + encoding = locale.getdefaultlocale()[1] or 'utf-8' + match = SHAREDLIB_RE.search(str(process.stdout.read(), encoding=encoding)) + if match: + return Version(match.groups()[0]) + + +def clean_egg_name(name): + """Remove Python version and platform name from Egg files/dirs. + + >>> clean_egg_name('python_pipeline-0.1.3_py3k-py3.1.egg-info') + 'python_pipeline-0.1.3_py3k.egg-info' + >>> clean_egg_name('Foo-1.2-py2.7-linux-x86_64.egg-info') + 'Foo-1.2.egg-info' + """ + match = EGGnPTH_RE.match(name) + if match and match.group(2) is not None: + return ''.join(match.group(1, 3)) + return name + + +def parse_ns(fpaths, other=None): + """Parse namespace_packages.txt files.""" + result = set(other or []) + for fpath in fpaths: + with open(fpath, 'r', encoding='utf-8') as fp: + for line in fp: + if line: + result.add(line.strip()) + return result + + +def remove_ns(interpreter, package, namespaces, versions): + """Remove empty __init__.py files for requested namespaces.""" + if not isinstance(namespaces, set): + namespaces = set(namespaces) + keep = set() + for ns in namespaces: + for version in versions: + fpath = join(interpreter.sitedir(package, version), *ns.split('.')) + fpath = join(fpath, '__init__.py') + if not exists(fpath): + continue + if getsize(fpath) != 0: + log.warning('file not empty, cannot share %s namespace', ns) + keep.add(ns) + break + + # return a set of namespaces that should be handled by pycompile/pyclean + result = namespaces - keep + + # remove empty __init__.py files, if available + for ns in result: + for version in versions: + dpath = join(interpreter.sitedir(package, version), *ns.split('.')) + fpath = join(dpath, '__init__.py') + if exists(fpath): + os.remove(fpath) + if not os.listdir(dpath): + os.rmdir(dpath) + # clean pyshared dir as well + dpath = join('debian', package, 'usr/share/pyshared', *ns.split('.')) + fpath = join(dpath, '__init__.py') + if exists(fpath): + os.remove(fpath) + if not os.listdir(dpath): + os.rmdir(dpath) + return result + + +def execute(command, cwd=None, env=None, log_output=None, shell=True): + """Execute external shell command. + + :param cdw: current 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 redirected to stdout/stderr + """ + args = {'shell': shell, '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', encoding='utf-8') + log_output.write('\n# command executed on {}'.format(datetime.now().isoformat())) + log_output.write('\n$ {}\n'.format(command)) + log_output.flush() + args.update(stdout=log_output, stderr=log_output) + + log.debug('invoking: %s', 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')) + + +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 pyinstall(interpreter, package, vrange): + """Install local files listed in pkg.pyinstall files as public modules.""" + srcfpath = "./debian/%s.pyinstall" % package + if not exists(srcfpath): + return + impl = interpreter.impl + versions = get_requested_versions(impl, vrange) + + for line in open(srcfpath, encoding='utf-8'): + if not line or line.startswith('#'): + continue + details = INSTALL_RE.match(line) + if not details: + raise ValueError("unrecognized line: %s" % line) + details = details.groupdict() + if details['module']: + details['module'] = details['module'].replace('.', '/') + myvers = versions & get_requested_versions(impl, details['vrange']) + if not myvers: + log.debug('%s.pyinstall: no matching versions for line %s', + package, line) + continue + files = glob(details['pattern']) + if not files: + raise ValueError("missing file(s): %s" % details['pattern']) + for fpath in files: + fpath = fpath.lstrip('/.') + if details['module']: + dstname = join(details['module'], split(fpath)[1]) + elif fpath.startswith('debian/'): + dstname = fpath[7:] + else: + dstname = fpath + for version in myvers: + dstfpath = join(interpreter.sitedir(package, version), dstname) + dstdir = split(dstfpath)[0] + if not exists(dstdir): + os.makedirs(dstdir) + if exists(dstfpath): + os.remove(dstfpath) + os.link(fpath, dstfpath) + + +def pyremove(interpreter, package, vrange): + """Remove public modules listed in pkg.pyremove file.""" + srcfpath = "./debian/%s.pyremove" % package + if not exists(srcfpath): + return + impl = interpreter.impl + versions = get_requested_versions(impl, vrange) + + for line in open(srcfpath, encoding='utf-8'): + if not line or line.startswith('#'): + continue + details = REMOVE_RE.match(line) + if not details: + raise ValueError("unrecognized line: %s: %s" % (package, line)) + details = details.groupdict() + myvers = versions & get_requested_versions(impl, details['vrange']) + if not myvers: + log.debug('%s.pyremove: no matching versions for line %s', + package, line) + for version in myvers: + site_dirs = interpreter.old_sitedirs(package, version) + site_dirs.append(interpreter.sitedir(package, version)) + for sdir in site_dirs: + files = glob(sdir + '/' + details['pattern']) + for fpath in files: + if isdir(fpath): + rmtree(fpath) + else: + os.remove(fpath) + +from dhpython.interpreter import Interpreter +from dhpython.version import Version, get_requested_versions, RANGE_PATTERN +INSTALL_RE = re.compile(r""" + (?P<pattern>.+?) # file pattern + (?:\s+ # optional Python module name: + (?P<module>[A-Za-z][A-Za-z0-9_.]*)? + )? + \s* # optional version range: + (?P<vrange>%s)?$ +""" % RANGE_PATTERN, re.VERBOSE) +REMOVE_RE = re.compile(r""" + (?P<pattern>.+?) # file pattern + \s* # optional version range: + (?P<vrange>%s)?$ +""" % RANGE_PATTERN, re.VERBOSE) diff --git a/dhpython/version.py b/dhpython/version.py new file mode 100644 index 0000000..98c16b7 --- /dev/null +++ b/dhpython/version.py @@ -0,0 +1,457 @@ +# Copyright © 2010-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 re +from os.path import exists + +from dhpython import _defaults + +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('dhpython') +Interpreter = None + + +class Version: + # TODO: Upgrade to PEP-440 + def __init__(self, value=None, major=None, minor=None, micro=None, + releaselevel=None, serial=None): + """Construct a new instance. + + >>> Version(major=0, minor=0, micro=0, releaselevel=0, serial=0) + Version('0.0') + >>> Version('0.0') + Version('0.0') + """ + 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 self.major is None: + 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): + try: + other = Version(other) + except Exception: + return False + 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 + + +class VersionRange: + def __init__(self, value=None, minver=None, maxver=None): + if minver: + self.minver = Version(minver) + else: + self.minver = None + if maxver: + self.maxver = Version(maxver) + else: + self.maxver = None + + if value: + minver, maxver = self.parse(value) + if minver and self.minver is None: + self.minver = minver + if maxver and self.maxver is None: + self.maxver = maxver + + def __bool__(self): + if self.minver is not None or self.maxver is not None: + return True + return False + + def __str__(self): + """Return version range string from given range. + + >>> str(VersionRange(minver='3.4')) + '3.4-' + >>> str(VersionRange(minver='3.4', maxver='3.6')) + '3.4-3.6' + >>> str(VersionRange(minver='3.4', maxver='4.0')) + '3.4-4.0' + >>> str(VersionRange(maxver='3.7')) + '-3.7' + >>> str(VersionRange(minver='3.5', maxver='3.5')) + '3.5' + >>> str(VersionRange()) + '-' + """ + if self.minver is None is self.maxver: + return '-' + if self.minver == self.maxver: + return str(self.minver) + elif self.minver is None: + return '-{}'.format(self.maxver) + elif self.maxver is None: + return '{}-'.format(self.minver) + else: + return '{}-{}'.format(self.minver, self.maxver) + + def __repr__(self): + """Return version range string. + + >>> repr(VersionRange('5.0-')) + "VersionRange(minver='5.0')" + >>> repr(VersionRange('3.0-3.5')) + "VersionRange(minver='3.0', maxver='3.5')" + """ + result = 'VersionRange(' + if self.minver is not None: + result += "minver='{}'".format(self.minver) + if self.maxver is not None: + result += ", maxver='{}'".format(self.maxver) + result = result.replace('(, ', '(') + return result + ")" + + @staticmethod + def parse(value): + """Return minimum and maximum Python version from given range. + + >>> VersionRange.parse('3.0-') + (Version('3.0'), None) + >>> VersionRange.parse('3.1-3.13') + (Version('3.1'), Version('3.13')) + >>> VersionRange.parse('3.2-4.0') + (Version('3.2'), Version('4.0')) + >>> VersionRange.parse('-3.7') + (None, Version('3.7')) + >>> VersionRange.parse('3.2') + (Version('3.2'), Version('3.2')) + >>> VersionRange.parse('') == VersionRange.parse('-') + True + >>> VersionRange.parse('>= 4.0') + (Version('4.0'), None) + """ + if value in ('', '-'): + return None, None + + match = RANGE_RE.match(value) + if not match: + try: + minv, maxv = VersionRange._parse_pycentral(value) + except Exception: + raise ValueError("version range is invalid: %s" % value) + else: + groups = match.groups() + + if list(groups).count(None) == 3: # only one version is allowed + minv = Version(groups[1]) + return minv, minv + + minv = maxv = None + if groups[0]: # maximum version only + maxv = groups[1] + else: + minv = groups[1] + maxv = groups[3] + + minv = Version(minv) if minv else None + maxv = Version(maxv) if maxv else None + + if maxv and minv and minv > maxv: + raise ValueError("version range is invalid: %s" % value) + + return minv, maxv + + @staticmethod + def _parse_pycentral(value): + """Parse X-Python3-Version. + + >>> VersionRange._parse_pycentral('>= 3.10') + (Version('3.10'), None) + >>> VersionRange._parse_pycentral('<< 4.0') + (None, Version('4.0')) + >>> VersionRange._parse_pycentral('3.1') + (Version('3.1'), Version('3.1')) + >>> VersionRange._parse_pycentral('3.1, 3.2') + (Version('3.1'), None) + """ + + minv = maxv = None + hardcoded = set() + + for item in value.split(','): + item = item.strip() + + match = re.match('>=\s*([\d\.]+)', item) + if match: + minv = match.group(1) + continue + match = re.match('<<\s*([\d\.]+)', item) + if match: + maxv = match.group(1) + continue + match = re.match('^[\d\.]+$', item) + if match: + hardcoded.add(match.group(0)) + + if len(hardcoded) == 1: + ver = hardcoded.pop() + return Version(ver), Version(ver) + + if not minv and hardcoded: + # yeah, no maxv! + minv = sorted(hardcoded)[0] + + return Version(minv) if minv else None, Version(maxv) if maxv else None + + +def default(impl): + """Return default interpreter version for given implementation.""" + if impl not in _defaults.DEFAULT: + raise ValueError("interpreter implementation not supported: %r" % impl) + ver = _defaults.DEFAULT[impl] + return Version(major=ver[0], minor=ver[1]) + + +def supported(impl): + """Return list of supported interpreter versions for given implementation.""" + if impl not in _defaults.SUPPORTED: + raise ValueError("interpreter implementation not supported: %r" % impl) + versions = _defaults.SUPPORTED[impl] + return [Version(major=v[0], minor=v[1]) for v in versions] + + +def get_requested_versions(impl, vrange=None, available=None): + """Return a set of requested and supported Python versions. + + :param impl: interpreter implementation + :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('cpython3', '')) == sorted(supported('cpython3')) + True + >>> sorted(get_requested_versions('cpython3', '-')) == sorted(supported('cpython3')) + True + >>> get_requested_versions('cpython3', '>= 5.0') + set() + """ + if isinstance(vrange, str): + vrange = VersionRange(vrange) + + if not vrange: + versions = set(supported(impl)) + else: + minv = Version(major=0, minor=0) if vrange.minver is None else vrange.minver + maxv = Version(major=99, minor=99) if vrange.maxver is None else vrange.maxver + if minv == maxv: + versions = set([minv] if minv in supported(impl) else tuple()) + else: + versions = set(v for v in supported(impl) if minv <= v < maxv) + + if available is not None: + # to avoid circular imports + global Interpreter + if Interpreter is None: + from dhpython.interpreter import Interpreter + if available: + interpreter = Interpreter(impl=impl) + versions = set(v for v in versions + if exists(interpreter.binary(v))) + elif available is False: + interpreter = Interpreter(impl=impl) + versions = set(v for v in versions + if not exists(interpreter.binary(v))) + + return versions + + +def build_sorted(versions, impl='cpython3'): + """Return sorted list of versions in a build friendly order. + + i.e. default version, if among versions, is sorted last. + + >>> build_sorted([(2, 6), (3, 4), default('cpython3'), (3, 6), (2, 7)])[-1] == default('cpython3') + True + >>> build_sorted(('3.2', (3, 0), '3.1')) + [Version('3.0'), Version('3.1'), Version('3.2')] + """ + default_ver = default(impl) + + result = sorted(Version(v) for v in versions) + try: + result.remove(default_ver) + except ValueError: + pass + else: + result.append(default_ver) + return result @@ -0,0 +1,602 @@ +#! /usr/bin/python3 +# vim: et ts=4 sw=4 +# 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 argparse +import re +import sys +from os import environ, getcwd, makedirs, remove +from os.path import abspath, exists, isdir, join +from shutil import rmtree +from tempfile import mkdtemp + +INTERP_VERSION_RE = re.compile(r'^python(?P<version>3\.\d+)(?P<dbg>-dbg)?$') +logging.basicConfig(format='%(levelname).1s: pybuild ' + '%(module)s:%(lineno)d: %(message)s') +log = logging.getLogger('dhpython') + + +def main(cfg): + log.debug('cfg: %s', cfg) + from dhpython import build, PKG_PREFIX_MAP + from dhpython.debhelper import DebHelper, build_options + from dhpython.version import Version, build_sorted, get_requested_versions + from dhpython.interpreter import Interpreter + from dhpython.tools import execute, move_matching_files + + if cfg.list_systems: + for name, Plugin in sorted(build.plugins.items()): + print(name, '\t', Plugin.DESCRIPTION) + exit(0) + + nocheck = False + if 'DEB_BUILD_OPTIONS' in environ: + nocheck = 'nocheck' in environ['DEB_BUILD_OPTIONS'] + if not nocheck and 'DEB_BUILD_PROFILES' in environ: + nocheck = 'nocheck' in environ['DEB_BUILD_PROFILES'] + + env = environ.copy() + # set some defaults in environ to make the build reproducible + env.setdefault('LC_ALL', 'C.UTF-8') + env.setdefault('CCACHE_DIR', abspath('.pybuild/ccache')) + env.setdefault('no_proxy', 'localhost') + if 'http_proxy' not in env: + env['http_proxy'] = 'http://127.0.0.1:9/' + elif not env['http_proxy']: + del env['http_proxy'] # some tools don't like empty var. + if 'https_proxy' not in env: + env['https_proxy'] = 'https://127.0.0.1:9/' + elif not env['https_proxy']: + del env['https_proxy'] # some tools don't like empty var. + if 'DEB_PYTHON_INSTALL_LAYOUT' not in env: + env['DEB_PYTHON_INSTALL_LAYOUT'] = 'deb' + + arch_data = {} + if exists('/usr/bin/dpkg-architecture'): + res = execute('/usr/bin/dpkg-architecture') + for line in res['stdout'].splitlines(): + key, value = line.strip().split('=', 1) + arch_data[key] = value + + # Set _PYTHON_HOST_PLATFORM to ensure debugging symbols on, f.e. i386 + # emded a constant name regardless of the 32/64-bit kernel. + host_platform = '{DEB_HOST_ARCH_OS}-{DEB_HOST_ARCH}'.format(**arch_data) + # it's not called amd64 in Python + host_platform = host_platform.replace('amd64', 'x86_64') + env.setdefault('_PYTHON_HOST_PLATFORM', host_platform) + + if arch_data['DEB_BUILD_ARCH'] != arch_data['DEB_HOST_ARCH']: + # support cross compiling Python 3.X extensions, see #892931 + env.setdefault('_PYTHON_SYSCONFIGDATA_NAME', + '_sysconfigdata__' + arch_data["DEB_HOST_MULTIARCH"]) + + # Selected on command line? + selected_plugin = cfg.system + + # Selected by build_dep? + if not selected_plugin: + dh = DebHelper(build_options()) + for build_dep in dh.build_depends: + if build_dep.startswith('pybuild-plugin-'): + selected_plugin = build_dep.split('-', 2)[2] + break + + if selected_plugin: + certainty = 99 + Plugin = build.plugins.get(selected_plugin) + if not Plugin: + log.error('unrecognized build system: %s', selected_plugin) + exit(10) + plugin = Plugin(cfg) + context = {'ENV': env, 'args': {}, 'dir': cfg.dir} + plugin.detect(context) + else: + plugin, certainty, context = None, 0, None + for Plugin in build.plugins.values(): + try: + tmp_plugin = Plugin(cfg) + except Exception as err: + log.warn('cannot initialize %s plugin: %s', Plugin.NAME, + err, exc_info=cfg.verbose) + continue + tmp_context = {'ENV': env, 'args': {}, 'dir': cfg.dir} + tmp_certainty = tmp_plugin.detect(tmp_context) + log.debug('Plugin %s: certainty %i', Plugin.NAME, tmp_certainty) + if tmp_certainty and tmp_certainty > certainty: + plugin, certainty, context = tmp_plugin, tmp_certainty, tmp_context + del Plugin + if not plugin: + log.error('cannot detect build system, please use --system option' + ' or set PYBUILD_SYSTEM env. variable') + exit(11) + + if plugin.SUPPORTED_INTERPRETERS is not True: + # if versioned interpreter was requested and selected plugin lists + # versioned ones as supported: extend list of supported interpreters + # with this interpreter + tpls = {i for i in plugin.SUPPORTED_INTERPRETERS if '{version}' in i} + if tpls: + for ipreter in cfg.interpreter: + m = INTERP_VERSION_RE.match(ipreter) + if m: + ver = m.group('version') + updated = set(tpl.format(version=ver) for tpl in tpls) + updated and plugin.SUPPORTED_INTERPRETERS.update(updated) + + for interpreter in cfg.interpreter: + if plugin.SUPPORTED_INTERPRETERS is not True and interpreter not in plugin.SUPPORTED_INTERPRETERS: + log.error('interpreter %s not supported by %s', interpreter, plugin) + exit(12) + log.debug('detected build system: %s (certainty: %s%%)', plugin.NAME, certainty) + + if cfg.detect_only: + if not cfg.really_quiet: + print(plugin.NAME) + exit(0) + + versions = cfg.versions + if not versions: + if len(cfg.interpreter) == 1: + i = cfg.interpreter[0] + m = INTERP_VERSION_RE.match(i) + if m: + log.debug('defaulting to version hardcoded in interpreter name') + versions = [m.group('version')] + else: + IMAP = {v: k for k, v in PKG_PREFIX_MAP.items()} + if i in IMAP: + versions = build_sorted(get_requested_versions( + IMAP[i], available=True), impl=IMAP[i]) + if versions and '{version}' not in i: + versions = versions[-1:] # last one, the default one + if not versions: # still no luck + log.debug('defaulting to all supported Python 3.X versions') + versions = build_sorted(get_requested_versions( + 'cpython3', available=True), impl='cpython3') + versions = [Version(v) for v in versions] + + def get_option(name, interpreter=None, version=None, default=None): + if interpreter: + # try PYBUILD_NAME_python3.3-dbg (or hardcoded interpreter) + i = interpreter.format(version=version or '') + opt = "PYBUILD_{}_{}".format(name.upper(), i) + if opt in environ: + return environ[opt] + # try PYBUILD_NAME_python3-dbg (if not checked above) + if '{version}' in interpreter and version: + i = interpreter.format(version=version.major) + opt = "PYBUILD_{}_{}".format(name.upper(), i) + if opt in environ: + return environ[opt] + # try PYBUILD_NAME + opt = "PYBUILD_{}".format(name.upper()) + if opt in environ: + return environ[opt] + # try command line args + return getattr(cfg, name, default) or default + + def get_args(context, step, version, interpreter): + i = interpreter.format(version=version) + ipreter = Interpreter(i) + + home_dir = [ipreter.impl, str(version)] + if ipreter.debug: + home_dir.append('dbg') + if cfg.name: + home_dir.append(cfg.name) + if cfg.autopkgtest_only: + base_dir = environ.get('AUTOPKGTEST_TMP') + if not base_dir: + base_dir = mkdtemp(prefix='pybuild-autopkgtest-') + else: + base_dir = '.pybuild/{}' + home_dir = base_dir.format('_'.join(home_dir)) + + build_dir = get_option('build_dir', interpreter, version, + default=join(home_dir, 'build')) + + destdir = context['destdir'].format(version=version, interpreter=i) + if cfg.name: + package = ipreter.suggest_pkg_name(cfg.name) + else: + package = 'PYBUILD_NAME_not_set' + if cfg.name and destdir.rstrip('/').endswith('debian/tmp'): + destdir = "debian/{}".format(package) + destdir = abspath(destdir) + + args = dict(context['args']) + args.update({ + 'package': package, + 'interpreter': ipreter, + 'version': version, + 'args': get_option("%s_args" % step, interpreter, version, ''), + 'dir': abspath(context['dir'].format(version=version, interpreter=i)), + 'destdir': destdir, + 'build_dir': abspath(build_dir.format(version=version, interpreter=i)), + # versioned dist-packages even for Python 3.X - dh_python3 will fix it later + # (and will have a chance to compare files) + 'install_dir': get_option('install_dir', interpreter, version, + '/usr/lib/python{version}/dist-packages' + ).format(version=version, interpreter=i), + 'home_dir': abspath(home_dir)}) + if interpreter == 'pypy': + args['install_dir'] = '/usr/lib/pypy/dist-packages/' + env = dict(args.get('ENV', {})) + pp = env.get('PYTHONPATH', context['ENV'].get('PYTHONPATH')) + pp = pp.split(':') if pp else [] + if step in {'build', 'test', 'autopkgtest'}: + if step in {'test', 'autopkgtest'}: + args['test_dir'] = join(args['destdir'], args['install_dir'].lstrip('/')) + if args['test_dir'] not in pp: + pp.append(args['test_dir']) + if args['build_dir'] not in pp: + pp.append(args['build_dir']) + # cross compilation support for Python 2.x + if (version.major == 2 and + arch_data.get('DEB_BUILD_ARCH') != arch_data.get('DEB_HOST_ARCH')): + pp.insert(0, ('/usr/lib/python{0}/plat-{1[DEB_HOST_MULTIARCH]}' + ).format(version, arch_data)) + env['PYTHONPATH'] = ':'.join(pp) + # cross compilation support for Python <= 3.8 (see above) + if version.major == 3: + name = '_PYTHON_SYSCONFIGDATA_NAME' + value = env.get(name, context['ENV'].get(name, '')) + if version << '3.8' and value.startswith('_sysconfigdata_')\ + and not value.startswith('_sysconfigdata_m'): + value = env[name] = "_sysconfigdata_m%s" % value[15:] + # update default from main() for -dbg interpreter + if value and ipreter.debug and not value.startswith('_sysconfigdata_d'): + env[name] = "_sysconfigdata_d%s" % value[15:] + args['ENV'] = env + + if not exists(args['build_dir']): + makedirs(args['build_dir']) + + return args + + def is_disabled(step, interpreter, version): + i = interpreter + prefix = "{}/".format(step) + disabled = (get_option('disable', i, version) or '').split() + for item in disabled: + if item in (step, '1'): + log.debug('disabling {} step for {} {}'.format(step, i, version)) + return True + if item.startswith(prefix): + disabled.append(item[len(prefix):]) + if i in disabled or str(version) in disabled or \ + i.format(version=version) in disabled or \ + i.format(version=version.major) in disabled: + log.debug('disabling {} step for {} {}'.format(step, i, version)) + return True + return False + + def run(func, interpreter, version, context): + step = func.__func__.__name__ + args = get_args(context, step, version, interpreter) + env = dict(context['ENV']) + if 'ENV' in args: + env.update(args['ENV']) + + before_cmd = get_option('before_{}'.format(step), interpreter, version) + if before_cmd: + if cfg.quiet: + log_file = join(args['home_dir'], 'before_{}_cmd.log'.format(step)) + else: + log_file = False + command = before_cmd.format(**args) + log.info(command) + output = execute(command, context['dir'], env, log_file) + if output['returncode'] != 0: + msg = 'exit code={}: {}'.format(output['returncode'], command) + raise Exception(msg) + + fpath = join(args['home_dir'], 'testfiles_to_rm_before_install') + if step == 'install' and exists(fpath): + with open(fpath) as fp: + for line in fp: + path = line.strip('\n') + if exists(path): + if isdir(path): + rmtree(path) + else: + remove(path) + remove(fpath) + result = func(context, args) + + after_cmd = get_option('after_{}'.format(step), interpreter, version) + if after_cmd: + if cfg.quiet: + log_file = join(args['home_dir'], 'after_{}_cmd.log'.format(step)) + else: + log_file = False + command = after_cmd.format(**args) + log.info(command) + output = execute(command, context['dir'], env, log_file) + if output['returncode'] != 0: + msg = 'exit code={}: {}'.format(output['returncode'], command) + raise Exception(msg) + return result + + def move_to_ext_destdir(i, version, context): + """Move built C extensions from the general destdir to ext_destdir""" + args = get_args(context, 'install', version, interpreter) + ext_destdir = get_option('ext_destdir', i, version) + if ext_destdir: + move_matching_files(args['destdir'], ext_destdir, + get_option('ext_pattern', i, version), + get_option('ext_sub_pattern', i, version), + get_option('ext_sub_repl', i, version)) + + func = None + if cfg.clean_only: + func = plugin.clean + elif cfg.configure_only: + func = plugin.configure + elif cfg.build_only: + func = plugin.build + elif cfg.install_only: + func = plugin.install + elif cfg.test_only: + func = plugin.test + elif cfg.autopkgtest_only: + func = plugin.test + elif cfg.print_args: + func = plugin.print_args + + ### one function for each interpreter at a time mode ### + if func: + step = func.__func__.__name__ + if step == 'test' and nocheck: + exit(0) + failure = False + for i in cfg.interpreter: + ipreter = Interpreter(interpreter.format(version=versions[0])) + iversions = build_sorted(versions, impl=ipreter.impl) + if '{version}' not in i and len(versions) > 1: + log.info('limiting Python versions to %s due to missing {version}' + ' in interpreter string', str(versions[-1])) + iversions = versions[-1:] # just the default or closest to default + for version in iversions: + if is_disabled(step, i, version): + continue + c = dict(context) + c['dir'] = get_option('dir', i, version, cfg.dir) + c['destdir'] = get_option('destdir', i, version, cfg.destdir) + try: + run(func, i, version, c) + except Exception as err: + log.error('%s: plugin %s failed with: %s', + step, plugin.NAME, err, exc_info=cfg.verbose) + # try to build/test other interpreters/versions even if + # one of them fails to make build logs more verbose: + failure = True + if step not in ('build', 'test', 'autopkgtest'): + exit(13) + if step == 'install': + move_to_ext_destdir(i, version, c) + if failure: + # exit with a non-zero return code if at least one build/test failed + exit(13) + exit(0) + + ### all functions for interpreters in batches mode ### + try: + context_map = {} + for i in cfg.interpreter: + ipreter = Interpreter(interpreter.format(version=versions[0])) + iversions = build_sorted(versions, impl=ipreter.impl) + if '{version}' not in i and len(versions) > 1: + log.info('limiting Python versions to %s due to missing {version}' + ' in interpreter string', str(versions[-1])) + iversions = versions[-1:] # just the default or closest to default + for version in iversions: + key = (i, version) + if key in context_map: + c = context_map[key] + else: + c = dict(context) + c['dir'] = get_option('dir', i, version, cfg.dir) + c['destdir'] = get_option('destdir', i, version, cfg.destdir) + context_map[key] = c + + if not is_disabled('clean', i, version): + run(plugin.clean, i, version, c) + if not is_disabled('configure', i, version): + run(plugin.configure, i, version, c) + if not is_disabled('build', i, version): + run(plugin.build, i, version, c) + if not is_disabled('install', i, version): + run(plugin.install, i, version, c) + move_to_ext_destdir(i, version, c) + if not nocheck and not is_disabled('test', i, version): + run(plugin.test, i, version, c) + except Exception as err: + log.error('plugin %s failed: %s', plugin.NAME, err, + exc_info=cfg.verbose) + exit(14) + + +def parse_args(argv): + usage = '%(prog)s [ACTION] [BUILD SYSTEM ARGS] [DIRECTORIES] [OPTIONS]' + parser = argparse.ArgumentParser(usage=usage) + parser.add_argument('-v', '--verbose', action='store_true', + default=environ.get('PYBUILD_VERBOSE') == '1', + help='turn verbose mode on') + parser.add_argument('-q', '--quiet', action='store_true', + default=environ.get('PYBUILD_QUIET') == '1', + help='doesn\'t show external command\'s output') + parser.add_argument('-qq', '--really-quiet', action='store_true', + default=environ.get('PYBUILD_RQUIET') == '1', + help='be quiet') + parser.add_argument('--version', action='version', version='%(prog)s DEVELV') + + action = parser.add_argument_group('ACTION', '''The default is to build, + install and test the library using detected build system version by + version. Selecting one of following actions, will invoke given action + for all versions - one by one - which (contrary to the default action) + in some build systems can overwrite previous results.''') + action.add_argument('--detect', action='store_true', dest='detect_only', + help='return the name of detected build system') + action.add_argument('--clean', action='store_true', dest='clean_only', + help='clean files using auto-detected build system specific methods') + action.add_argument('--configure', action='store_true', dest='configure_only', + help='invoke configure step for all requested Python versions') + action.add_argument('--build', action='store_true', dest='build_only', + help='invoke build step for all requested Python versions') + action.add_argument('--install', action='store_true', dest='install_only', + help='invoke install step for all requested Python versions') + action.add_argument('--test', action='store_true', dest='test_only', + help='invoke tests for auto-detected build system') + action.add_argument('--autopkgtest', action='store_true', dest='autopkgtest_only', + help='invoke autopkgtests for auto-detected build system') + action.add_argument('--list-systems', action='store_true', + help='list available build systems and exit') + action.add_argument('--print', action='append', dest='print_args', + help="print pybuild's internal parameters") + + arguments = parser.add_argument_group('BUILD SYSTEM ARGS', ''' + Additional arguments passed to the build system. + --system=custom requires complete command.''') + arguments.add_argument('--before-clean', metavar='CMD', + help='invoked before the clean command') + arguments.add_argument('--clean-args', metavar='ARGS') + arguments.add_argument('--after-clean', metavar='CMD', + help='invoked after the clean command') + + arguments.add_argument('--before-configure', metavar='CMD', + help='invoked before the configure command') + arguments.add_argument('--configure-args', metavar='ARGS') + arguments.add_argument('--after-configure', metavar='CMD', + help='invoked after the configure command') + + arguments.add_argument('--before-build', metavar='CMD', + help='invoked before the build command') + arguments.add_argument('--build-args', metavar='ARGS') + arguments.add_argument('--after-build', metavar='CMD', + help='invoked after the build command') + + arguments.add_argument('--before-install', metavar='CMD', + help='invoked before the install command') + arguments.add_argument('--install-args', metavar='ARGS') + arguments.add_argument('--after-install', metavar='CMD', + help='invoked after the install command') + + arguments.add_argument('--before-test', metavar='CMD', + help='invoked before the test command') + arguments.add_argument('--test-args', metavar='ARGS') + arguments.add_argument('--after-test', metavar='CMD', + help='invoked after the test command') + + tests = parser.add_argument_group('TESTS', '''\ + unittest\'s discover is used by default (if available)''') + tests.add_argument('--test-nose', action='store_true', + default=environ.get('PYBUILD_TEST_NOSE') == '1', + help='use nose module in --test step') + tests.add_argument('--test-nose2', action='store_true', + default=environ.get('PYBUILD_TEST_NOSE2') == '1', + help='use nose2 module in --test step') + tests.add_argument('--test-pytest', action='store_true', + default=environ.get('PYBUILD_TEST_PYTEST') == '1', + help='use pytest module in --test step') + tests.add_argument('--test-tox', action='store_true', + default=environ.get('PYBUILD_TEST_TOX') == '1', + help='use tox in --test step') + tests.add_argument('--test-custom', action='store_true', + default=environ.get('PYBUILD_TEST_CUSTOM') == '1', + help='use custom command in --test step') + + dirs = parser.add_argument_group('DIRECTORIES') + dirs.add_argument('-d', '--dir', action='store', metavar='DIR', + default=environ.get('PYBUILD_DIR', getcwd()), + help='source files directory - base for other relative dirs [default: CWD]') + dirs.add_argument('--dest-dir', action='store', metavar='DIR', dest='destdir', + default=environ.get('DESTDIR', 'debian/tmp'), + help='destination directory [default: debian/tmp]') + dirs.add_argument('--ext-dest-dir', action='store', metavar='DIR', dest='ext_destdir', + default=environ.get('PYBUILD_EXT_DESTDIR'), + help='destination directory for .so files') + dirs.add_argument('--ext-pattern', action='store', metavar='PATTERN', + default=environ.get('PYBUILD_EXT_PATTERN', r'\.so(\.[^/]*)?$'), + help='regular expression for files that should be moved' + ' if --ext-dest-dir is set [default: .so files]') + dirs.add_argument('--ext-sub-pattern', action='store', metavar='PATTERN', + default=environ.get('PYBUILD_EXT_SUB_PATTERN'), + help='pattern to change --ext-pattern\'s filename or path') + dirs.add_argument('--ext-sub-repl', action='store', metavar='PATTERN', + default=environ.get('PYBUILD_EXT_SUB_REPL'), + help='replacement for match from --ext-sub-pattern,' + ' empty string by default') + dirs.add_argument('--install-dir', action='store', metavar='DIR', + help='installation directory [default: .../dist-packages]') + dirs.add_argument('--name', action='store', + default=environ.get('PYBUILD_NAME'), + help='use this name to guess destination directories') + + limit = parser.add_argument_group('LIMITATIONS') + limit.add_argument('-s', '--system', + default=environ.get('PYBUILD_SYSTEM'), + help='select a build system [default: auto-detection]') + limit.add_argument('-p', '--pyver', action='append', dest='versions', + help='''build for Python VERSION. + This option can be used multiple times + [default: all supported Python 3.X versions]''') + limit.add_argument('-i', '--interpreter', action='append', + help='change interpreter [default: python{version}]') + limit.add_argument('--disable', metavar='ITEMS', + help='disable action, interpreter or version') + + args = parser.parse_args() + if not args.interpreter: + args.interpreter = environ.get('PYBUILD_INTERPRETERS', 'python{version}').split() + if not args.versions: + args.versions = environ.get('PYBUILD_VERSIONS', '').split() + else: + # add support for -p `pyversions -rv` + versions = [] + for version in args.versions: + versions.extend(version.split()) + args.versions = versions + + if args.test_nose or args.test_nose2 or args.test_pytest or args.test_tox\ + or args.test_custom or args.system == 'custom': + args.custom_tests = True + else: + args.custom_tests = False + + return args + + +if __name__ == '__main__': + cfg = parse_args(sys.argv) + if cfg.really_quiet: + cfg.quiet = True + log.setLevel(logging.CRITICAL) + elif cfg.verbose: + log.setLevel(logging.DEBUG) + else: + log.setLevel(logging.INFO) + log.debug('version: DEVELV') + log.debug(sys.argv) + main(cfg) + # let dh/cdbs clean the .pybuild dir + # rmtree(join(cfg.dir, '.pybuild')) diff --git a/pybuild-autopkgtest b/pybuild-autopkgtest new file mode 100755 index 0000000..5b3af8d --- /dev/null +++ b/pybuild-autopkgtest @@ -0,0 +1,68 @@ +#! /usr/bin/env perl +# vim: et ts=4 sw=4 +# Copyright © 2021 Antonio Terceiro <terceiro@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. + +use strict; +use warnings; +use File::Basename; +use File::Temp qw( tempdir ); +use Debian::Debhelper::Buildsystem::pybuild; +use Debian::Debhelper::Dh_Lib qw(doit); + +sub main { + my $tmpdir = tempdir( CLEANUP => 1); + my $run = "${tmpdir}/run"; + open(RUN, ">", $run) or die($!); + print(RUN "#!/usr/bin/make -f\n"); + print(RUN "include debian/rules\n"); + print(RUN "pybuild-autopkgtest:\n"); + printf(RUN "\tpybuild-autopkgtest\n"); + close(RUN); + chmod(0755, $run); + $ENV{PYBUILD_AUTOPKGTEST} = "1"; + if (system("grep -q ^before-pybuild-autopkgtest: debian/rules") == 0) { + doit($run, "before-pybuild-autopkgtest"); + } + doit($run, "pybuild-autopkgtest"); + if (system("grep -q ^after-pybuild-autopkgtest: debian/rules") == 0) { + doit($run, "after-pybuild-autopkgtest"); + } +} + +sub child { + # The child inherits the environment defined in debian/rules + my $dh = Debian::Debhelper::Buildsystem::pybuild->new(); + foreach my $command ($dh->pybuild_commands('autopkgtest')) { + doit(@$command); + } +} + +if (scalar(@ARGV) > 0) { + my $prog = basename($0); + print STDERR "usage: ${prog}\n"; + exit(1); +} + +if (defined $ENV{PYBUILD_AUTOPKGTEST}) { + child; +} else { + main; +} diff --git a/pybuild-autopkgtest.rst b/pybuild-autopkgtest.rst new file mode 100644 index 0000000..76da53d --- /dev/null +++ b/pybuild-autopkgtest.rst @@ -0,0 +1,109 @@ +===================== + pybuild-autopkgtest +===================== + +---------------------------------------------------------------------------------------------------- +invokes the test suite against requested Python versions and installed packages +---------------------------------------------------------------------------------------------------- + +:Manual section: 1 +:Author: Antonio Terceiro, 2021 + +SYNOPSIS +======== + pybuild-autopkgtest + +OPTIONS +======= + +`pybuild-autopkgtest` takes no options or arguments. All configuration is done +via the same mechanisms you use to control `pybuild` itself, namely by having +build dependencies on the right packages, or by setting `PYBUILD_*` environment +variables in `debian/rules`. + +DESCRIPTION +=========== + +`pybuild-autopkgtest` is an autopkgtest test runner that reuses all the +`pybuild` infrastructure to run tests against installed packages, as expected +by autopkgtest. To enable `pybuild-autopkgtest` for your package, you need to +add **Testsuite: autopkgtest-pkg-pybuild** to the source stanza in +`debian/control`. This will cause autodep8(1) to produce the correct contents +for `debian/tests/control`. + +`pybuild-autopkgtest` will run the tests in exactly the same way as `pybuild` +will during the build, with the exception that the tests are not run in the +build directory. The test files are copied to a temporary directory, so that +the tests will run against the installed Python modules, and not against +anything in the source tree. + +All the pybuild infrastructure is used, so for most packages you don't need to +add any extra code configure for `pybuild-autopkgtest`. For example, just +having a `python3-pytest` as a build dependency is enough to make the test +runner use `pytest` to run the tests. + +The tests are executed via a temporary makefile that includes `debian/rules` +from the package, so that any environment variables defined there will also be +available during autopkgtest, including but not limited to `PYBUILD_*` +variables for configuring the behavior of `pybuild` itself. + +ADOPTING PYBUILD-AUTOPKGTEST +============================ + +Since `pybuild-autopkgtest` reuses environment variables set in `debian/rules`, +migrating packages to use `pybuild-autopkgtest` should not require much effort. + +You might have a `debian/tests/control` that duplicates what +`pybuild-autopkgtest` already does, e.g. copying the test files to a temporary +directory, changing to it, and running the tests from there. Such test entries +can usually be removed in favor of adding **Testsuite: +autopkgtest-pkg-pybuild** to `debian/control`. + +In general, you want to move any test-related command line arguments to pybuild +into environment variables in `debian/rules`. + +You can also have specialized, manually-written test cases, alongside the ones +autogenerated by `autodep8`. For this, both set **Testsuite: +autopkgtest-pkg-pybuild** in `debian/control` and keep your custom tests in +`debian/tests/control`. + +VARYING BEHAVIOR UNDER AUTOPKGTEST +================================== + +Ideally, the behavior of the tests should be the same during the build and +under autopkgtest, except for the fact that during autopkgtest the tests should +load the code from the installed package. `pybuild-autopkgtest` should do this +correctly most of the time. + +There are situations, however, in which you need a slightly different behavior +during the autopkgtest run. There are a few mechanisms to support that: + +- `pybuild-autopkgtest` sets the `PYBUILD_AUTOPKGTEST` environment variable to + `1` during the test run. This way, you can add conditional behavior in + `debian/rules`. +- Before and after running the tests, `pybuild-autopkgtest` will call the + `debian/rules` targets `before-pybuild-autopkgtest` and + `after-pybuild-autopkgtest`, respectively, if they exist. + +SAMPLE TEST CONTROL FILE +======================== + +The control file produced by autodep8(1) looks like this:: + + Test-Command: pybuild-autopkgtest + Depends: @, @builddeps@, + Restrictions: allow-stderr, skippable, + Features: test-name=pybuild-autopkgtest + +You should never need to hardcode this in `debian/tests/control`. You can add +extra items to `Restrictions` and `Depends` by providing a configuration file +for `autodep8` (`debian/tests/autopkgtest-pkg-pybuild.conf`) like this:: + + extra_depends=foo, bar + extra_restrictions=isolation-container, breaks-testbed + +SEE ALSO +======== +* pybuild(1) +* autopkgtest(1) +* autodep8(1) diff --git a/pybuild.rst b/pybuild.rst new file mode 100644 index 0000000..f60db1f --- /dev/null +++ b/pybuild.rst @@ -0,0 +1,306 @@ +========= + pybuild +========= + +---------------------------------------------------------------------------------------------------- +invokes various build systems for requested Python versions in order to build modules and extensions +---------------------------------------------------------------------------------------------------- + +:Manual section: 1 +:Author: Piotr Ożarowski, 2012-2019 + +SYNOPSIS +======== + pybuild [ACTION] [BUILD SYSTEM ARGUMENTS] [DIRECTORIES] [OPTIONS] + +DEBHELPER COMMAND SEQUENCER INTEGRATION +======================================= +* build depend on `dh-python`, +* build depend on all supported Python interpreters, pybuild will use it to create + a list of interpreters to build for. + Recognized dependencies: + + - `python3-all-dev` - for Python extensions that work with Python 3.X interpreters, + - `python3-all-dbg` - as above, add this one if you're building -dbg packages, + - `python3-all` - for Python modules that work with Python 3.X interpreters, + - `python3-dev` - builds an extension for default Python 3.X interpreter + (useful for private extensions, use python3-all-dev for public ones), + - `python3` - as above, used if headers files are not needed to build private module, + +* add `--buildsystem=pybuild` to dh's arguments in debian/rules, +* if more than one binary package is build: + add debian/python3-foo.install files, or + `export PYBUILD_NAME=modulename` (modulename will be used to guess binary + package prefixes), or + `export PYBUILD_DESTDIR` env. variables in debian/rules +* add `--with=python3` to dh's arguments in debian/rules + (see proper helper's manpage for more details) or add `dh-sequence-python3` + to Build-Depends + +debian/rules file example:: + + #! /usr/bin/make -f + export PYBUILD_NAME=foo + %: + dh $@ --with python3 --buildsystem=pybuild + +OPTIONS +======= + Most options can be set (in addition to command line) via environment + variables. PyBuild will check: + + * PYBUILD_OPTION_VERSIONED_INTERPRETER (f.e. PYBUILD_CLEAN_ARGS_python3.2) + * PYBUILD_OPTION_INTERPRETER (f.e. PYBUILD_CONFIGURE_ARGS_python3-dbg) + * PYBUILD_OPTION (f.e. PYBUILD_INSTALL_ARGS) + +optional arguments +------------------ + -h, --help show this help message and exit + -v, --verbose turn verbose mode on + -q, --quiet doesn't show external command's output + -qq, --really-quiet be quiet + --version show program's version number and exit + +ACTION +------ + The default is to build, install and test the library using detected build + system version by version. Selecting one of following actions, will invoke + given action for all versions - one by one - which (contrary to the default + action) in some build systems can overwrite previous results. + + --detect + return the name of detected build system + --clean + clean files using auto-detected build system specific methods + --configure + invoke configure step for all requested Python versions + --build + invoke build step for all requested Python versions + --install + invoke install step for all requested Python versions + --test + invoke tests for auto-detected build system + --list-systems + list available build systems and exit + --print + print pybuild's internal parameters + +TESTS +----- + unittest's discover from standard library is used in test step by default. + + --test-nose + use nose module in test step, remember to add python-nose and/or + python3-nose to Build-Depends + --test-nose2 + use nose2 module in test step, remember to add python-nose2 and/or + python3-nose2 to Build-Depends + --test-pytest + use pytest module in test step, remember to add python-pytest and/or + python3-pytest to Build-Depends + --test-tox + use tox command in test step, remember to add tox + to Build-Depends. Requires tox.ini file + --test-custom + use a custom command in the test step. The full test command is then + specified with `--test-args` or by setting the `PYBUILD_TEST_ARGS` + environment variable. Remember to add any needed packages to run the + tests to Build-Depends. + + +testfiles +~~~~~~~~~ + Tests are invoked from within build directory to make sure newly built + files are tested instead of source files. If test suite requires other files + in this directory, you can list them in `debian/pybuild.testfiles` file + (you can also use `debian/pybuild_pythonX.testfiles` or + `debian/pybuild_pythonX.Y.testfiles`) and files listed there will be copied + before test step and removed before install step. + By default only `test` and `tests` directories are copied to build directory. + +BUILD SYSTEM ARGUMENTS +---------------------- + Additional arguments passed to the build system. + --system=custom requires complete command in --foo-args parameters. + + --before-clean COMMAND + invoked before the clean command + --clean-args ARGUMENTS + arguments added to clean command generated by build system plugin + --after-clean COMMAND + invoked after the clean command + --before-configure COMMAND + invoked before the configure command + --configure-args ARGUMENTS + arguments added to configure command generated by build system plugin + --after-configure COMMAND + invoked after the configure command + --before-build COMMAND + invoked before the build command + --build-args ARGUMENTS + arguments added to build command generated by build system plugin + --after-build COMMAND + invoked after the build command + --before-install COMMAND + invoked before the install command + --install-args ARGUMENTS + arguments added to install command generated by build system plugin + --after-install COMMAND + invoked after the install command + --before-test COMMAND + invoked before the test command + --test-args ARGUMENTS + arguments added to test command generated by build system plugin + --after-test COMMAND + invoked after the test command + +variables that can be used in `ARGUMENTS` and `COMMAND` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +* `{version}` will be replaced with current Python version, + you can also use `{version.major}`, `{version.minor}`, etc. +* `{interpreter}` will be replaced with current interpreter, + you can also use `{interpreter.include_dir}` +* `{dir}` will be replaced with sources directory, +* `{destdir}` will be replaced with destination directory, +* `{home_dir}` will be replaced with temporary HOME directory, + where plugins can keep their data + (.pybuild/interpreter_version/ by default), +* `{build_dir}` will be replaced with build directory +* `{install_dir}` will be replaced with install directory. +* `{package}` will be replaced with suggested package name, + if --name (or PYBUILD_NAME) is set to `foo`, this variable + will be replaced with `python3-foo`. + +DIRECTORIES +----------- + -d DIR, --dir DIR + set source files directory - base for other relative dirs + [by default: current working directory] + --dest-dir DIR + set destination directory [default: debian/tmp] + --ext-dest-dir DIR + set destination directory for .so files + --ext-pattern PATTERN + regular expression for files that should be moved if --ext-dest-dir is set + [default: `\.so(\.[^/]*)?$`] + --ext-sub-pattern PATTERN + regular expression for part of path/filename matched in --ext-pattern + that should be removed or replaced with --ext-sub-repl + --ext-sub-repl PATTERN + replacement for matches in --ext-sub-pattern + --install-dir DIR + set installation directory [default: .../dist-packages] + --name NAME + use this name to guess destination directories + ("foo" sets debian/python3-foo) + This overrides --dest-dir. + +variables that can be used in `DIR` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +* `{version}` will be replaced with current Python version, +* `{interpreter}` will be replaced with selected interpreter. + +LIMITATIONS +----------- + -s SYSTEM, --system SYSTEM + select a build system [default: auto-detection] + -p VERSIONS, --pyver VERSIONS + build for Python VERSIONS. This option can be used multiple times. + Versions can be separated by space character. + The default is all Python 3.X supported versions. + -i INTERPRETER, --interpreter INTERPRETER + change interpreter [default: python{version}] + --disable ITEMS + disable action, interpreter, version or any mix of them. + Note that f.e. python3 and python3-dbg are two different interpreters, + --disable test/python3 doesn't disable python3-dbg's tests. + +disable examples +~~~~~~~~~~~~~~~~ +* `--disable test/python3.9-dbg` - disables tests for python3.9-dbg +* `--disable '3.8 3.9'` - disables all actions for version 3.8 and 3.9 +* `PYBUILD_DISABLE=python3.9` - disables all actions for Python 3.9 +* `PYBUILD_DISABLE_python3.3=test` - disables tests for Python 3.3 +* `PYBUILD_DISABLE=test/python3.3` - same as above +* `PYBUILD_DISABLE=configure/python3 3.2` - disables configure + action for all python3 interpreters, and all actions for version 3.2 + + +PLUGINS +------- +pybuild supports multiple build system plugins. By default it is +automatically selected. These systems are currently supported:: + +* distutils (most commonly used) +* cmake +* flit (deprecated) +* pyproject +* custom + +flit plugin +~~~~~~~~~~~ +The flit plugin is deprecated, please use the pyproject plugin instead. + +The flit plugin can be used to build Debian packages based on PEP 517 +metadata in `pyproject.toml` when flit is the upstream build system. These +can be identified by the presence of a `build-backend = "flit_core.buildapi"` +element in `pyproject.toml`. The flit plugin only supports python3. To use +this plugin:: + +* build depend on `flit` and either +* build depend on `python3-tomli` so flit can be automatically selected or +* add `export PYBUILD_SYSTEM=flit` to debian/rules to manually select + +debian/rules file example:: + + #! /usr/bin/make -f + export PYBUILD_NAME=foo + export PYBUILD_SYSTEM=flit (needed if python3-tomli is not installed) + %: + dh $@ --with python3 --buildsystem=pybuild + +pyproject +~~~~~~~~~ +The pyproject plugin drives the new PEP-517 standard interface for +building Python packages, upstream. This is configured via +`pyproject.toml`. +This plugin is expected to replace the distutils and flit plugins in the +future. +The entry points generated by the package are created during the build step +(other plugins make the entry points during the install step); the entry +points are available in PATH during the test step, permitting them to be +called from tests. + +To use this plugin: + +* build depend on `pybuild-plugin-pyproject` as well as any build tools + specified by upstream in `pyproject.toml`. + +ENVIRONMENT +=========== + +As described above in OPTIONS, pybuild can be configured by `PYBUILD_` +prefixed environment variables. + +Tests are skipped if `nocheck` is in the `DEB_BUILD_OPTIONS` or +`DEB_BUILD_PROFILES` environment variables. + +`DESTDIR` provides a default a default value to the `--dest-dir` option. + +Pybuild will export `http_proxy=http://127.0.0.1:9/`, +`https_proxy=https://127.0.0.1:9/`, and `no_proxy=localhost` to +hopefully block attempts by the package's build-system to access the +Internet. +If network access to a loopback interface is needed and blocked by this, +export empty `http_proxy` and `https_proxy` variables before calling +pybuild. + +If not set, `LC_ALL`, `CCACHE_DIR`, `DEB_PYTHON_INSTALL_LAYOUT`, +`_PYTHON_HOST_PLATFORM`, `_PYTHON_SYSCONFIGDATA_NAME`, will all be set +to appropriate values, before calling the package's build script. + +SEE ALSO +======== +* dh_python3(1) +* https://wiki.debian.org/Python/Pybuild +* http://deb.li/pybuild - most recent version of this document diff --git a/pydist/Makefile b/pydist/Makefile new file mode 100644 index 0000000..2932a45 --- /dev/null +++ b/pydist/Makefile @@ -0,0 +1,18 @@ +#!/usr/bin/make -f + +FALLBACK_FLAGS = $(shell dpkg-vendor --is ubuntu && echo '--ubuntu') + +all: cpython3_fallback README.PyDist.html + +clean: + rm -rf cache + #rm -f dist_fallback + rm -f README.PyDist.html + +cpython3_fallback: + python3 ./generate_fallback_list.py $(FALLBACK_FLAGS) + +README.PyDist.html: README.PyDist + rst2html $< $@ + +.PHONY: clean diff --git a/pydist/README.PyDist b/pydist/README.PyDist new file mode 100644 index 0000000..dad3853 --- /dev/null +++ b/pydist/README.PyDist @@ -0,0 +1,112 @@ +============ +PyDist files +============ + +DISTNAME [VRANGE] [DEPENDENCY][; [PEP386] [RULES]] +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +PyDist files help tools like dh_python3 to translate Python dependencies +(from setup.py's install_requires or egg's requires.txt file) to Debian +dependencies if given Python distribution file / directory is not installed +(hint: add proper package to Build-Depends and PyDist file might not be needed) +or if detection is not correct. + +Before checking for package that provides installed egg-info file or directory +dh_python3 is checking these locations for overrides: + + * debian/py3dist-overrides + * /usr/share/python3/dist/* + * /usr/share/dh-python/dist/cpython3_fallback + +debian/python3-foo.pydist is copied into /usr/share/python3/dist/ automatically. + +*NOTE:* There's no need to add an override if build-depending on a package that +provides searched egg-info results in correctly recognized dependency. + +Required fields: +~~~~~~~~~~~~~~~~ + +DISTNAME +```````` +Python distribution name (you can find it at the beginning of .egg-info +file/directory name that your package provides). + +Examples: + * SQLAlchemy + * Jinja2 + * numpy + + +Optional fields: +~~~~~~~~~~~~~~~~ + +VRANGE +`````` +Python version or version range the line applies to. + +Examples: + * 2.6 (Python 2.6 only) + * 2.5- (Python 2.5 and newer) + * 2.5-2.7 (Python 2.5 or 2.6) + * -2.7 (Python 2.6 or older) + + * 3.1 (Python 3.1 only) + * 3.1- (Python 3.1 and newer) + * 3.1-3.3 (Python 3.1 or 3.2) + * -3.4 (Python 3.3 or older) + +DEPENDENCY +`````````` +Debian dependency, multiple packages or versions are allowed. +If not set, given Python distribution name will be ignored. + +Examples: + * python-mako + * python-jinja2 | python (>= 2.6) + * python-sqlalchemy (>= 0.5), python-sqlalchemy (<< 0.6) + + * python3-mako + * python3-jinja2 | python3 (>= 3.0) + * python3-sqlalchemy (>= 0.5), python3-sqlalchemy (<< 0.6) + +PEP386 +`````` +Standards flag: upstream uses versioning schema described in PEP 386. + +RULES +````` +Rules needed to translate upstream version to Debian one. If PEP386 is +set, its rules will be applied later. Multiple rules are allowed, separate them +with a space. + +Examples: + * s/^/2:/ + * s/alpha/~alpha/ s/^/1:/ + + +Notes: +~~~~~~ + +You can use multiple lines if binary package provides more than one Python +distribution or if you want to specify different dependencies for each Python +version or version range. + +If you use dh_python3, it will install debian/binary_package_name.pydist file +to /usr/share/dh-python/dist/cpython3/binary_package_name automatically. + + +Complete examples: +~~~~~~~~~~~~~~~~~~ + * SQLAlchemy python-sqlalchemy (>= 0.5), python-sqlalchemy (<< 0.6) + * Mako python-mako; PEP386 + * foo -2.5 python-oldfoo; s/^/3:/ + * foo 2.5- python-foo; PEP386 + * Bar 2.6- + + * SQLAlchemy python3-sqlalchemy (>= 0.5), python3-sqlalchemy (<< 0.6) + * Mako python3-mako; PEP386 + * foo -3.2 python3-oldfoo; s/^/3:/ + * foo 3.2- python3-foo; PEP386 + * Bar 2.6- + +.. vim: ft=rst diff --git a/pydist/cpython3_fallback b/pydist/cpython3_fallback new file mode 100644 index 0000000..263cae2 --- /dev/null +++ b/pydist/cpython3_fallback @@ -0,0 +1,5173 @@ +2ping 2ping +APScheduler python3-apscheduler +AnyQt python3-anyqt +Arpeggio python3-arpeggio +Arriero arriero +Authlib python3-authlib +Automat python3-automat +BTrees python3-btrees +BUSCO busco +Babel python3-babel +BabelGladeExtractor python3-babelgladeextractor +Beaker python3-beaker +Biosig python3-biosig +Bootstrap_Flask python3-flask-bootstrap +Bottleneck python3-bottleneck +Brian2 python3-brian +Brlapi python3-brlapi +Brotli python3-brotli +BuildStream python3-buildstream +BuildStream_external python3-bst-external +CAI python3-cai +CCColUtils python3-cccolutils +CDApplet cairo-dock-dbus-plug-in-interface-python +CDBashApplet cairo-dock-dbus-plug-in-interface-python +CMOR python3-cmor +CNVkit cnvkit +CSXCAD python3-openems +CT3 python3-cheetah +CTDopts python3-ctdopts +CXX python3-cxx-dev +CacheControl python3-cachecontrol +CairoSVG python3-cairosvg +Cartopy python3-cartopy +Cerberus python3-cerberus +Cerealizer python3-cerealizer +Chameleon python3-chameleon +CheMPS2 python3-chemps2 +CherryPy python3-cherrypy3 +ClusterShell python3-clustershell +CommonMark_bkrs python3-commonmark-bkrs +ConfigArgParse python3-configargparse +ConsensusCore python3-pbconsensuscore +Cython cython3 +DBussy python3-dbussy +DNApi python3-dnapilib +DSV python3-dsv +DateTimeRange python3-datetimerange +Decopy decopy +DendroPy python3-dendropy +Deprecated python3-deprecated +DisplayCAL displaycal +Distance python3-distance +Django python3-django +DoubleRatchet python3-doubleratchet +EXtra_data python3-extra-data +EasyProcess python3-easyprocess +EbookLib python3-ebooklib +Editobj3 python3-editobj3 +EditorConfig python3-editorconfig +Electrum python3-electrum +Endgame_Singularity singularity +ExifRead python3-exifread +Extractor python3-extractor +Faker python3-fake-factory +FinalCif finalcif +Fiona python3-fiona +Flask python3-flask +Flask_API python3-flask-api +Flask_AppBuilder python3-flask-appbuilder +Flask_AutoIndex python3-flask-autoindex +Flask_Babel python3-flask-babel +Flask_BabelEx python3-flask-babelex +Flask_BasicAuth python3-flask-basicauth +Flask_Bcrypt python3-flask-bcrypt +Flask_Caching python3-flask-caching +Flask_Compress python3-flask-compress +Flask_Cors python3-flask-cors +Flask_FlatPages python3-flask-flatpages +Flask_Gravatar python3-flask-gravatar +Flask_HTMLmin python3-flask-htmlmin +Flask_HTTPAuth python3-flask-httpauth +Flask_JWT_Extended python3-python-flask-jwt-extended +Flask_JWT_Simple python3-flask-jwt-simple +Flask_LDAPConn python3-flask-ldapconn +Flask_Limiter python3-flask-limiter +Flask_Login python3-flask-login +Flask_Mail python3-flask-mail +Flask_Migrate python3-flask-migrate +Flask_OpenID python3-flask-openid +Flask_Paranoid python3-flask-paranoid +Flask_Principal python3-flask-principal +Flask_RESTful python3-flask-restful +Flask_SQLAlchemy python3-flask-sqlalchemy +Flask_Security_Too python3-flask-security +Flask_Seeder python3-flask-seeder +Flask_Session python3-flask-session +Flask_Silk python3-flask-silk +Flask_SocketIO python3-flask-socketio +Flask_Sockets python3-flask-sockets +Flask_Testing python3-flask-testing +Flask_WTF python3-flaskext.wtf +Flor python3-flor +FormEncode python3-formencode +Frozen_Flask python3-frozen-flask +FsQuota python3-fsquota +GDAL python3-gdal +GaussSum gausssum +GenX python3-genx +GenomeTools python3-genometools +Genshi python3-genshi +GeoAlchemy2 python3-geoalchemy2 +GeoIP python3-geoip +Geophar geophar +GitPython python3-git +GladTeX python3-gleetex +Glances glances +Glymur python3-glymur +GooCalendar python3-goocalendar +Grammalecte_fr python3-grammalecte +GridDataFormats python3-griddataformats +Gyoto python3-gyoto +HTSeq python3-htseq +HeapDict python3-heapdict +HyperKitty python3-django-hyperkitty +IMDbPY python3-imdbpy +IPy python3-ipy +InSilicoSeq insilicoseq +Isenkram isenkram-cli +IsoSpecPy python3-isospec +JACK_Client python3-jack-client +JCC python3-jcc +JPype1 python3-jpype +JUBE jube +Jinja2 python3-jinja2 +Kaptive kaptive +Keras python3-keras +Keras_Applications python3-keras-applications +Keras_Preprocessing python3-keras-preprocessing +Kivy python3-kivy +Kleborate kleborate +Kyoto_Cabinet python3-kyotocabinet +LEPL python3-lepl +Lektor lektor +LibAppArmor python3-libapparmor +LinkChecker linkchecker +Logbook python3-logbook +Loom brz-loom +M2Crypto python3-m2crypto +MACS2 macs +MDAnalysis python3-mdanalysis +MDP python3-mdp +MIDIUtil python3-midiutil +MMLlib python3-mmllib +MPD_sima mpd-sima +MacSyFinder macsyfinder +Magics python3-magics++ +Magnus magnus +Mako python3-mako +ManimPango python3-manimpango +MapProxy python3-mapproxy +Markdown python3-markdown +MarkupPy python3-markuppy +MarkupSafe python3-markupsafe +Markups python3-markups +Mastodon.py python3-mastodon +MechanicalSoup python3-mechanicalsoup +MetaPhlAn metaphlan +MicrobeGPS microbegps +MiniMock python3-minimock +Mirage mirage +Mnemosyne mnemosyne +MontagePy python3-montagepy +Mopidy mopidy +Mopidy_ALSAMixer mopidy-alsamixer +Mopidy_Beets mopidy-beets +Mopidy_InternetArchive mopidy-internetarchive +Mopidy_Local mopidy-local +Mopidy_MPD mopidy-mpd +Mopidy_MPRIS mopidy-mpris +Mopidy_Podcast mopidy-podcast +Mopidy_Podcast_iTunes mopidy-podcast-itunes +Mopidy_Scrobbler mopidy-scrobbler +Mopidy_SomaFM mopidy-somafm +Mopidy_SoundCloud mopidy-soundcloud +Mopidy_TuneIn mopidy-tunein +Mopidy_dLeyna mopidy-dleyna +Morfessor python3-morfessor +MutatorMath python3-mutatormath +NanoFilt nanofilt +NanoLyse nanolyse +NanoSV nanosv +NanoStat python3-nanostat +NeXus python3-nxs +Nik4 nik4 +Nuitka nuitka +OBITools obitools +OMEMO python3-omemo +OWSLib python3-owslib +OdooRPC python3-odoorpc +Oldmemo python3-oldmemo +Onionbalance onionbalance +OpenLP openlp +OpenMM python3-simtk +OptimiR optimir +PAM python3-pam +PGPy python3-pgpy +PHCpy python3-phcpy +POT python3-pot +PTable python3-ptable +Pallets_Sphinx_Themes python3-pallets-sphinx-themes +ParmEd python3-parmed +Parsley python3-parsley +Paste python3-paste +PasteDeploy python3-pastedeploy +PasteScript python3-pastescript +Pattern python3-pattern +PeachPy python3-peachpy +PeakUtils python3-peakutils +Pebble python3-pebble +PeptideBuilder python3-peptidebuilder +Pillow python3-pil +Pint python3-pint +Pivy python3-pivy +Plinth freedombox +Pmw python3-pmw +Printrun printrun-common +ProDy python3-prody +Protego python3-protego +PsychoPy psychopy +PuLP python3-pulp +Pweave python3-pweave +Py3ODE python3-pyode +PyAVM python3-pyavm +PyAudio python3-pyaudio +PyBindGen python3-pybindgen +PyBluez python3-bluez +PyChess pychess +PyChromecast python3-pychromecast +PyCifRW python3-pycifrw +PyDispatcher python3-pydispatch +PyDrive2 python3-pydrive2 +PyGObject python3-gi +PyGithub python3-github +PyGnuplot python3-pygnuplot +PyGreSQL python3-pygresql +PyHamcrest python3-hamcrest +PyHoca_CLI pyhoca-cli +PyHoca_GUI pyhoca-gui +PyICU python3-icu +PyJWT python3-jwt +PyKCS11 python3-pykcs11 +PyKMIP python3-pykmip +PyLD python3-pyld +PyMca5 python3-pymca5 +PyMeasure python3-pymeasure +PyMeeus python3-pymeeus +PyMemoize python3-memoize +PyMuPDF python3-fitz +PyMySQL python3-pymysql +PyNLPl python3-pynlpl +PyNN python3-pynn +PyNaCl python3-nacl +PyNamecheap python3-namecheap +PyOpenGL python3-opengl +PyPrind python3-pyprind +PyPump python3-pypump +PyQRCode python3-pyqrcode +PyQSO pyqso +PyQt5 python3-pyqt5 +PyQt5_sip python3-pyqt5.sip +PyQt6 python3-pyqt6 +PyQt6_QScintilla python3-pyqt6.qsci +PyQt6_WebEngine python3-pyqt6.qtwebengine +PyQt6_sip python3-pyqt6.sip +PyQtWebEngine python3-pyqt5.qtwebengine +PyQt_Qwt python3-pyqt5.qwt +PyQt_builder python3-pyqtbuild +PyRSS2Gen python3-pyrss2gen +PySDL2 python3-sdl2 +PySPH python3-pysph +PySimpleSOAP python3-pysimplesoap +PySocks python3-socks +PyStaticConfiguration python3-staticconf +PyStemmer python3-stemmer +PyTrie python3-trie +PyUtilib python3-pyutilib +PyVCF python3-vcf +PyVISA python3-pyvisa +PyVISA_py python3-pyvisa-py +PyVirtualDisplay python3-pyvirtualdisplay +PyWavefront python3-pywavefront +PyWavelets python3-pywt +PyWebDAV3 python3-webdav +PyX python3-pyx +PyXRD python3-pyxrd +PyYAML python3-yaml +PyZoltan python3-pyzoltan +Pygments python3-pygments +Pymacs pymacs +Pyment python3-pyment +Pympler python3-pympler +Pypubsub python3-pubsub +Pyro4 python3-pyro4 +PythonQwt python3-qwt +QDarkStyle python3-qdarkstyle +QScintilla python3-pyqt5.qsci +QtAwesome python3-qtawesome +QtPy python3-qtpy +Quamash python3-quamash +QuantLib quantlib-python +ROPGadget python3-ropgadget +RPi.bme280 python3-bme280 +Radicale python3-radicale +ReParser python3-reparser +Recoll python3-recoll +Ren_Py python3-renpy +RestrictedPython python3-restrictedpython +Routes python3-routes +Rtree python3-rtree +RunSnakeRun runsnakerun +Rx python3-rx +SCons scons +SPARQLWrapper python3-sparqlwrapper +SQLAlchemy python3-sqlalchemy +SQLAlchemy_Utc python3-sqlalchemy-utc +SQLAlchemy_Utils python3-sqlalchemy-utils +SQLAlchemy_i18n python3-sqlalchemy-i18n +SQLObject python3-sqlobject +SaltPyLint python3-saltpylint +SciPy python3-scipy +Scrapy python3-scrapy +SecretStorage python3-secretstorage +SecureString python3-securestring +Send2Trash python3-send2trash +Shapely python3-shapely +Shredder rmlint-gui +SimPy python3-simpy +SimpleTAL python3-simpletal +SocksipyChain python3-socksipychain +SoftLayer python3-softlayer +Sonata sonata +SquareMap python3-squaremap +Stetl python3-stetl +Sublist3r sublist3r +Supysonic supysonic +TPOT python3-tpot +Telethon python3-telethon +Tempita python3-tempita +Theano python3-theano +TkinterTreectrl python3-tktreectrl +Trac trac +Trololio python3-trololio +Twisted python3-twisted +Twomemo python3-twomemo +TxSNI python3-txsni +URLObject python3-urlobject +Unidecode python3-unidecode +UnknownHorizons unknown-horizons +UpSetPlot python3-upsetplot +VF_1 vf1 +VMDKstream python3-vmdkstream +VirtualMailManager vmm +WALinuxAgent waagent +WSGIProxy2 python3-wsgiproxy +WSME python3-wsme +WTForms python3-wtforms +WTForms_Alchemy python3-wtforms-alchemy +WTForms_Components python3-wtforms-components +WTForms_JSON python3-wtforms-json +WTForms_Test python3-wtforms-test +Wand python3-wand +WebOb python3-webob +WebTest python3-webtest +Werkzeug python3-werkzeug +Whoosh python3-whoosh +Wikkid python3-wikkid +Willow python3-willow +X3DH python3-x3dh +XEdDSA python3-xeddsa +XRStools python3-xrstools +XStatic python3-xstatic +XStatic_Angular python3-xstatic-angular +XStatic_Angular_Bootstrap python3-xstatic-angular-bootstrap +XStatic_Angular_Cookies python3-xstatic-angular-cookies +XStatic_Angular_FileUpload python3-xstatic-angular-fileupload +XStatic_Angular_Gettext python3-xstatic-angular-gettext +XStatic_Angular_Mock python3-xstatic-angular-mock +XStatic_Angular_Schema_Form python3-xstatic-angular-schema-form +XStatic_Angular_UUID python3-xstatic-angular-uuid +XStatic_Angular_Vis python3-xstatic-angular-vis +XStatic_Angular_lrdragndrop python3-xstatic-angular-lrdragndrop +XStatic_Bootstrap_Datepicker python3-xstatic-bootstrap-datepicker +XStatic_Bootstrap_SCSS python3-xstatic-bootstrap-scss +XStatic_D3 python3-xstatic-d3 +XStatic_Dagre python3-xstatic-dagre +XStatic_Dagre_D3 python3-xstatic-dagre-d3 +XStatic_FileSaver python3-xstatic-filesaver +XStatic_Font_Awesome python3-xstatic-font-awesome +XStatic_Graphlib python3-xstatic-graphlib +XStatic_Hogan python3-xstatic-hogan +XStatic_JQuery.Bootstrap.Wizard python3-xstatic-jquery.bootstrap.wizard +XStatic_JQuery.TableSorter python3-xstatic-jquery.tablesorter +XStatic_JQuery.quicksearch python3-xstatic-jquery.quicksearch +XStatic_JQuery_Migrate python3-xstatic-jquery-migrate +XStatic_JSEncrypt python3-xstatic-jsencrypt +XStatic_JS_Yaml python3-xstatic-js-yaml +XStatic_Jasmine python3-xstatic-jasmine +XStatic_Json2yaml python3-xstatic-json2yaml +XStatic_Magic_Search python3-xstatic-magic-search +XStatic_Moment_Timezone python3-xstatic-moment-timezone +XStatic_QUnit python3-xstatic-qunit +XStatic_Rickshaw python3-xstatic-rickshaw +XStatic_Spin python3-xstatic-spin +XStatic_angular_ui_router python3-xstatic-angular-ui-router +XStatic_bootswatch python3-xstatic-bootswatch +XStatic_jQuery python3-xstatic-jquery +XStatic_jquery_ui python3-xstatic-jquery-ui +XStatic_lodash python3-xstatic-lodash +XStatic_mdi python3-xstatic-mdi +XStatic_moment python3-xstatic-moment +XStatic_objectpath python3-xstatic-objectpath +XStatic_roboto_fontface python3-xstatic-roboto-fontface +XStatic_smart_table python3-xstatic-smart-table +XStatic_term.js python3-xstatic-term.js +XStatic_tv4 python3-xstatic-tv4 +X_Tile x-tile +XlsxWriter python3-xlsxwriter +Yapps2 python3-yapps +Yapsy python3-yapsy +YubiOTP python3-yubiotp +ZooKeeper python3-zookeeper +_pycbf python3-pycbf +a38 python3-a38 +aafigure python3-aafigure +absl_py python3-absl +abydos python3-abydos +acme python3-acme +acme_tiny acme-tiny +acora python3-acora +actdiag python3-actdiag +actionlib python3-actionlib +actionlib_tools python3-actionlib-tools +activipy python3-activipy +adal python3-adal +adapt_parser python3-adapt +adios_mpi python3-adios +admesh python3-admesh +advocate python3-advocate +aeidon python3-aeidon +afdko python3-afdko +afew afew +affine python3-affine +agate python3-agate +agate_dbf python3-agatedbf +agate_excel python3-agateexcel +agate_sql python3-agatesql +aggdraw python3-aggdraw +aio_pika python3-aio-pika +aioamqp python3-aioamqp +aioapns python3-aioapns +aiocoap python3-aiocoap +aiodns python3-aiodns +aiodogstatsd python3-aiodogstatsd +aiofiles python3-aiofiles +aioftp python3-aioftp +aiohttp python3-aiohttp +aiohttp_apispec python3-aiohttp-apispec +aiohttp_cors python3-aiohttp-cors +aiohttp_jinja2 python3-aiohttp-jinja2 +aiohttp_mako python3-aiohttp-mako +aiohttp_oauthlib python3-aiohttp-oauthlib +aiohttp_openmetrics python3-aiohttp-openmetrics +aiohttp_proxy python3-aiohttp-proxy +aiohttp_security python3-aiohttp-security +aiohttp_session python3-aiohttp-session +aiohttp_socks python3-aiohttp-socks +aiohttp_wsgi python3-aiohttp-wsgi +aioice python3-aioice +aioinflux python3-aioinflux +aiomeasures python3-aiomeasures +aiomysql python3-aiomysql +aionotify python3-aionotify +aioopenssl python3-aioopenssl +aiopg python3-aiopg +aioprocessing python3-aioprocessing +aioredis python3-aioredis +aioresponses python3-aioresponses +aiormq python3-aiormq +aiorpcX python3-aiorpcx +aiortc python3-aiortc +aiorwlock python3-aiorwlock +aiosasl python3-aiosasl +aiosignal python3-aiosignal +aiosmtpd python3-aiosmtpd +aiosmtplib python3-aiosmtplib +aiosqlite python3-aiosqlite +aiostream python3-aiostream +aiotask_context python3-aiotask-context +aioxmlrpc python3-aioxmlrpc +aioxmpp python3-aioxmpp +aiozipkin python3-aiozipkin +aiozmq python3-aiozmq +airr python3-airr +airspeed python3-airspeed +ajpy python3-ajpy +alabaster python3-alabaster +alembic python3-alembic +alignlib python3-alignlib +allpairspy python3-allpairspy +altair python3-altair +altgraph python3-altgraph +ament_clang_format python3-ament-clang-format +ament_clang_tidy python3-ament-clang-tidy +ament_cmake_google_benchmark python3-ament-cmake-google-benchmark +ament_cmake_test python3-ament-cmake-test +ament_copyright python3-ament-copyright +ament_cppcheck python3-ament-cppcheck +ament_cpplint python3-ament-cpplint +ament_flake8 python3-ament-flake8 +ament_index_python python3-ament-index +ament_lint python3-ament-lint +ament_lint_cmake python3-ament-lint-cmake +ament_mypy python3-ament-mypy +ament_package python3-ament-package +ament_pep257 python3-ament-pep257 +ament_pycodestyle python3-ament-pycodestyle +ament_pyflakes python3-ament-pyflakes +ament_uncrustify python3-ament-uncrustify +ament_xmllint python3-ament-xmllint +amp_atomistics python3-amp +amply python3-amply +amqp python3-amqp +amqplib python3-amqplib +androguard androguard +angles python3-angles +aniso8601 python3-aniso8601 +anndata python3-anndata +annexremote python3-annexremote +anonip anonip +anosql python3-anosql +ansi python3-ansi +ansible ansible +ansible_compat python3-ansible-compat +ansible_core ansible-core +ansible_lint ansible-lint +ansible_pygments python3-ansible-pygments +ansible_runner python3-ansible-runner +ansicolors python3-colors +ansimarkup python3-ansimarkup +antlr python3-antlr +antlr4_python3_runtime python3-antlr4 +anyio python3-anyio +anyjson python3-anyjson +anymarkup python3-anymarkup +anymarkup_core python3-anymarkup-core +aodh python3-aodh +aodhclient python3-aodhclient +apache_libcloud python3-libcloud +apertium python3-apertium-core +apertium_apy apertium-apy +apertium_lex_tools python3-apertium-lex-tools +apertium_streamparser python3-streamparser +apipkg python3-apipkg +apispec python3-apispec +apksigcopier apksigcopier +aplpy python3-aplpy +apparmor python3-apparmor +appdirs python3-appdirs +applicationinsights python3-applicationinsights +apprise apprise +apptools python3-apptools +apsw python3-apsw +apt_clone apt-clone +apt_offline apt-offline +apt_venv apt-venv +apt_xapian_index apt-xapian-index +aptfs aptfs +aptly_api_client python3-aptly-api-client +ara python3-ara +arabic_reshaper python3-arabic-reshaper +arandr arandr +archmage archmage +arcp python3-arcp +argcomplete python3-argcomplete +argh python3-argh +argon2_cffi python3-argon2 +argparse python3 (>= 3.2) +argparse_manpage python3-argparse-manpage +args python3-args +ariba ariba +arpy python3-arpy +arpys python3-arpys +arrayfire python3-arrayfire +arrow python3-arrow +artifacts python3-artifacts +asciinema asciinema +asciitree python3-asciitree +asdf python3-asdf +asdf_astropy python3-asdf-astropy +asdf_coordinates_schemas python3-asdf-coordinates-schemas +asdf_standard python3-asdf-standard +asdf_transform_schemas python3-asdf-transform-schemas +asdf_wcs_schemas python3-asdf-wcs-schemas +ase python3-ase +asgiref python3-asgiref +asn1crypto python3-asn1crypto +astLib python3-astlib +asteval python3-asteval +astor python3-astor +astral python3-astral +astroML python3-astroml +astroalign python3-astroalign +astrodendro python3-astrodendro +astroid python3-astroid +astroplan python3-astroplan +astropy python3-astropy +astropy_healpix python3-astropy-healpix +astropy_helpers python3-astropy-helpers +astropy_sphinx_theme python3-astropy-sphinx-theme +astroquery python3-astroquery +astroscrappy python3-astroscrappy +asttokens python3-asttokens +astunparse python3-astunparse +async_generator python3-async-generator +async_lru python3-async-lru +async_timeout python3-async-timeout +asyncio_mqtt python3-asyncio-mqtt +asyncpg python3-asyncpg +asyncssh python3-asyncssh +atomicwrites python3-atomicwrites +atpublic python3-public +atropos atropos +attrs python3-attr +aubio python3-aubio +audioread python3-audioread +audiotools audiotools +authheaders python3-authheaders +authprogs authprogs +authres python3-authres +auto_editor auto-editor +autobahn python3-autobahn +autocommand python3-autocommand +autoflake autoflake +autoimport autoimport +autokey autokey-common +automaton python3-automaton +autopage python3-autopage +autopep8 python3-autopep8 +autoradio autoradio +autosuspend autosuspend +av python3-av +avro python3-avro +awesomeversion python3-awesomeversion +aws_requests_auth python3-aws-requests-auth +aws_shell aws-shell +aws_xray_sdk python3-aws-xray-sdk +awscli awscli +awscrt python3-awscrt +ayatana_settings ayatana-settings +ayatanawebmail ayatana-webmail +azure_agrifood_farming python3-azure +azure_ai_anomalydetector python3-azure +azure_ai_formrecognizer python3-azure +azure_ai_language_conversations python3-azure +azure_ai_language_questionanswering python3-azure +azure_ai_metricsadvisor python3-azure +azure_ai_ml python3-azure +azure_ai_personalizer python3-azure +azure_ai_textanalytics python3-azure +azure_ai_translation_document python3-azure +azure_appconfiguration python3-azure +azure_appconfiguration_provider python3-azure +azure_applicationinsights python3-azure +azure_batch python3-azure +azure_cli python3-azure-cli +azure_cli_core python3-azure-cli-core +azure_cli_telemetry python3-azure-cli-telemetry +azure_cli_testsdk python3-azure-cli-testsdk +azure_cognitiveservices_anomalydetector python3-azure +azure_cognitiveservices_formrecognizer python3-azure +azure_cognitiveservices_knowledge_qnamaker python3-azure +azure_cognitiveservices_language_luis python3-azure +azure_cognitiveservices_language_spellcheck python3-azure +azure_cognitiveservices_language_textanalytics python3-azure +azure_cognitiveservices_personalizer python3-azure +azure_cognitiveservices_search_autosuggest python3-azure +azure_cognitiveservices_search_customimagesearch python3-azure +azure_cognitiveservices_search_customsearch python3-azure +azure_cognitiveservices_search_entitysearch python3-azure +azure_cognitiveservices_search_imagesearch python3-azure +azure_cognitiveservices_search_newssearch python3-azure +azure_cognitiveservices_search_videosearch python3-azure +azure_cognitiveservices_search_visualsearch python3-azure +azure_cognitiveservices_search_websearch python3-azure +azure_cognitiveservices_vision_computervision python3-azure +azure_cognitiveservices_vision_contentmoderator python3-azure +azure_cognitiveservices_vision_customvision python3-azure +azure_cognitiveservices_vision_face python3-azure +azure_common python3-azure +azure_communication_chat python3-azure +azure_communication_email python3-azure +azure_communication_identity python3-azure +azure_communication_jobrouter python3-azure +azure_communication_networktraversal python3-azure +azure_communication_phonenumbers python3-azure +azure_communication_rooms python3-azure +azure_communication_sms python3-azure +azure_confidentialledger python3-azure +azure_containerregistry python3-azure +azure_core python3-azure +azure_core_experimental python3-azure +azure_cosmos python3-azure-cosmos +azure_cosmosdb_table python3-azure-cosmosdb-table +azure_data_tables python3-azure +azure_datalake_store python3-azure-datalake-store +azure_developer_devcenter python3-azure +azure_developer_loadtesting python3-azure +azure_devops python3-azext-devops +azure_devtools python3-azure-devtools +azure_digitaltwins_core python3-azure +azure_eventgrid python3-azure +azure_eventhub python3-azure +azure_eventhub_checkpointstoreblob python3-azure +azure_eventhub_checkpointstoreblob_aio python3-azure +azure_eventhub_checkpointstoretable python3-azure +azure_functions_devops_build python3-azure-functions-devops-build +azure_graphrbac python3-azure +azure_identity python3-azure +azure_iot_deviceupdate python3-azure +azure_iot_modelsrepository python3-azure +azure_keyvault python3-azure +azure_keyvault_administration python3-azure +azure_keyvault_certificates python3-azure +azure_keyvault_keys python3-azure +azure_keyvault_secrets python3-azure +azure_kusto_data python3-azure-kusto-data +azure_loganalytics python3-azure +azure_maps_geolocation python3-azure +azure_maps_render python3-azure +azure_maps_route python3-azure +azure_maps_search python3-azure +azure_media_analytics_edge python3-azure +azure_media_videoanalyzer_edge python3-azure +azure_messaging_webpubsubservice python3-azure +azure_mgmt_advisor python3-azure +azure_mgmt_agfood python3-azure +azure_mgmt_agrifood python3-azure +azure_mgmt_alertsmanagement python3-azure +azure_mgmt_apimanagement python3-azure +azure_mgmt_app python3-azure +azure_mgmt_appcomplianceautomation python3-azure +azure_mgmt_appconfiguration python3-azure +azure_mgmt_appcontainers python3-azure +azure_mgmt_applicationinsights python3-azure +azure_mgmt_appplatform python3-azure +azure_mgmt_attestation python3-azure +azure_mgmt_authorization python3-azure +azure_mgmt_automanage python3-azure +azure_mgmt_automation python3-azure +azure_mgmt_avs python3-azure +azure_mgmt_azureadb2c python3-azure +azure_mgmt_azurearcdata python3-azure +azure_mgmt_azurestack python3-azure +azure_mgmt_azurestackhci python3-azure +azure_mgmt_baremetalinfrastructure python3-azure +azure_mgmt_batch python3-azure +azure_mgmt_batchai python3-azure +azure_mgmt_billing python3-azure +azure_mgmt_billingbenefits python3-azure +azure_mgmt_botservice python3-azure +azure_mgmt_cdn python3-azure +azure_mgmt_changeanalysis python3-azure +azure_mgmt_chaos python3-azure +azure_mgmt_cognitiveservices python3-azure +azure_mgmt_commerce python3-azure +azure_mgmt_communication python3-azure +azure_mgmt_compute python3-azure +azure_mgmt_confidentialledger python3-azure +azure_mgmt_confluent python3-azure +azure_mgmt_connectedvmware python3-azure +azure_mgmt_consumption python3-azure +azure_mgmt_containerinstance python3-azure +azure_mgmt_containerregistry python3-azure +azure_mgmt_containerservice python3-azure +azure_mgmt_core python3-azure +azure_mgmt_cosmosdb python3-azure +azure_mgmt_costmanagement python3-azure +azure_mgmt_customproviders python3-azure +azure_mgmt_dashboard python3-azure +azure_mgmt_databox python3-azure +azure_mgmt_databoxedge python3-azure +azure_mgmt_databricks python3-azure +azure_mgmt_datadog python3-azure +azure_mgmt_datafactory python3-azure +azure_mgmt_datalake_analytics python3-azure +azure_mgmt_datalake_store python3-azure +azure_mgmt_datamigration python3-azure +azure_mgmt_dataprotection python3-azure +azure_mgmt_datashare python3-azure +azure_mgmt_deploymentmanager python3-azure +azure_mgmt_desktopvirtualization python3-azure +azure_mgmt_devcenter python3-azure +azure_mgmt_deviceupdate python3-azure +azure_mgmt_devspaces python3-azure +azure_mgmt_devtestlabs python3-azure +azure_mgmt_digitaltwins python3-azure +azure_mgmt_dns python3-azure +azure_mgmt_dnsresolver python3-azure +azure_mgmt_documentdb python3-azure +azure_mgmt_dynatrace python3-azure +azure_mgmt_edgegateway python3-azure +azure_mgmt_edgeorder python3-azure +azure_mgmt_education python3-azure +azure_mgmt_elastic python3-azure +azure_mgmt_elasticsan python3-azure +azure_mgmt_eventgrid python3-azure +azure_mgmt_eventhub python3-azure +azure_mgmt_extendedlocation python3-azure +azure_mgmt_fluidrelay python3-azure +azure_mgmt_frontdoor python3-azure +azure_mgmt_guestconfig python3-azure +azure_mgmt_hanaonazure python3-azure +azure_mgmt_hdinsight python3-azure +azure_mgmt_healthbot python3-azure +azure_mgmt_healthcareapis python3-azure +azure_mgmt_hybridcompute python3-azure +azure_mgmt_hybridkubernetes python3-azure +azure_mgmt_hybridnetwork python3-azure +azure_mgmt_imagebuilder python3-azure +azure_mgmt_iotcentral python3-azure +azure_mgmt_iothub python3-azure +azure_mgmt_iothubprovisioningservices python3-azure +azure_mgmt_keyvault python3-azure +azure_mgmt_kubernetesconfiguration python3-azure +azure_mgmt_kusto python3-azure +azure_mgmt_labservices python3-azure +azure_mgmt_loadtesting python3-azure +azure_mgmt_loganalytics python3-azure +azure_mgmt_logic python3-azure +azure_mgmt_logz python3-azure +azure_mgmt_machinelearningcompute python3-azure +azure_mgmt_machinelearningservices python3-azure +azure_mgmt_maintenance python3-azure +azure_mgmt_managedservices python3-azure +azure_mgmt_managementgroups python3-azure +azure_mgmt_managementpartner python3-azure +azure_mgmt_maps python3-azure +azure_mgmt_marketplaceordering python3-azure +azure_mgmt_media python3-azure +azure_mgmt_mixedreality python3-azure +azure_mgmt_mobilenetwork python3-azure +azure_mgmt_monitor python3-azure +azure_mgmt_msi python3-azure +azure_mgmt_netapp python3-azure +azure_mgmt_network python3-azure +azure_mgmt_networkfunction python3-azure +azure_mgmt_nginx python3-azure +azure_mgmt_notificationhubs python3-azure +azure_mgmt_oep python3-azure +azure_mgmt_operationsmanagement python3-azure +azure_mgmt_orbital python3-azure +azure_mgmt_peering python3-azure +azure_mgmt_policyinsights python3-azure +azure_mgmt_portal python3-azure +azure_mgmt_powerbidedicated python3-azure +azure_mgmt_powerbiembedded python3-azure +azure_mgmt_privatedns python3-azure +azure_mgmt_purview python3-azure +azure_mgmt_quantum python3-azure +azure_mgmt_quota python3-azure +azure_mgmt_rdbms python3-azure +azure_mgmt_recoveryservices python3-azure +azure_mgmt_recoveryservicesbackup python3-azure +azure_mgmt_recoveryservicessiterecovery python3-azure +azure_mgmt_redhatopenshift python3-azure +azure_mgmt_redis python3-azure +azure_mgmt_redisenterprise python3-azure +azure_mgmt_regionmove python3-azure +azure_mgmt_relay python3-azure +azure_mgmt_reservations python3-azure +azure_mgmt_resource python3-azure +azure_mgmt_resourceconnector python3-azure +azure_mgmt_resourcegraph python3-azure +azure_mgmt_resourcehealth python3-azure +azure_mgmt_resourcemover python3-azure +azure_mgmt_scheduler python3-azure +azure_mgmt_scvmm python3-azure +azure_mgmt_search python3-azure +azure_mgmt_security python3-azure +azure_mgmt_securitydevops python3-azure +azure_mgmt_securityinsight python3-azure +azure_mgmt_serialconsole python3-azure +azure_mgmt_servermanager python3-azure +azure_mgmt_servicebus python3-azure +azure_mgmt_servicefabric python3-azure +azure_mgmt_servicefabricmanagedclusters python3-azure +azure_mgmt_servicelinker python3-azure +azure_mgmt_signalr python3-azure +azure_mgmt_sql python3-azure +azure_mgmt_sqlvirtualmachine python3-azure +azure_mgmt_storage python3-azure +azure_mgmt_storagecache python3-azure +azure_mgmt_storageimportexport python3-azure +azure_mgmt_storagepool python3-azure +azure_mgmt_storagesync python3-azure +azure_mgmt_streamanalytics python3-azure +azure_mgmt_subscription python3-azure +azure_mgmt_support python3-azure +azure_mgmt_synapse python3-azure +azure_mgmt_testbase python3-azure +azure_mgmt_timeseriesinsights python3-azure +azure_mgmt_trafficmanager python3-azure +azure_mgmt_videoanalyzer python3-azure +azure_mgmt_vmwarecloudsimple python3-azure +azure_mgmt_web python3-azure +azure_mgmt_webpubsub python3-azure +azure_mgmt_workloadmonitor python3-azure +azure_mgmt_workloads python3-azure +azure_mixedreality_authentication python3-azure +azure_mixedreality_remoterendering python3-azure +azure_monitor_ingestion python3-azure +azure_monitor_opentelemetry_exporter python3-azure +azure_monitor_query python3-azure +azure_multiapi_storage python3-azure-multiapi-storage +azure_purview_administration python3-azure +azure_purview_catalog python3-azure +azure_purview_scanning python3-azure +azure_schemaregistry python3-azure +azure_schemaregistry_avroencoder python3-azure +azure_search_documents python3-azure +azure_security_attestation python3-azure +azure_servicebus python3-azure +azure_servicefabric python3-azure +azure_servicemanagement_legacy python3-azure +azure_storage_blob python3-azure-storage +azure_storage_blob_changefeed python3-azure-storage +azure_storage_file_datalake python3-azure-storage +azure_storage_file_share python3-azure-storage +azure_storage_queue python3-azure-storage +azure_synapse python3-azure +azure_synapse_accesscontrol python3-azure +azure_synapse_artifacts python3-azure +azure_synapse_managedprivateendpoints python3-azure +azure_synapse_monitoring python3-azure +azure_synapse_spark python3-azure +azure_template python3-azure +b2 backblaze-b2 +b2sdk python3-b2sdk +b4 b4 +babelfish python3-babelfish +backcall python3-backcall +backdoor_factory backdoor-factory +backoff python3-backoff +backup2swift python3-backup2swift +backupchecker backupchecker +banal python3-banal +bandit python3-bandit +banking.statements.nordea ofxstatement-plugins +banking.statements.osuuspankki ofxstatement-plugins +barbican python3-barbican +barbican_tempest_plugin barbican-tempest-plugin +barectf python3-barectf +barman python3-barman +baron python3-baron +base58 python3-base58 +basemap python3-mpltoolkits.basemap +bashate python3-bashate +bayespy python3-bayespy +bcbio_gff python3-bcbio-gff +bcc python3-bpfcc +bcdoc python3-bcdoc +bcolz python3-bcolz +bcrypt python3-bcrypt +bdebstrap bdebstrap +bdfproxy bdfproxy +bdist_nsi python3-bdist-nsi +bdsf python3-bdsf +beanbag python3-beanbag +beanbag_docutils python3-beanbag-docutils +beancount python3-beancount +beautifulsoup4 python3-bs4 +behave python3-behave +bel_resources python3-bel-resources +beniget python3-beniget +bepasty bepasty +bernhard python3-bernhard +berrynet python3-berrynet +betamax python3-betamax +beziers python3-beziers +bibtexparser python3-bibtexparser +bidict python3-bidict +bids_validator python3-bids-validator +billiard python3-billiard +binaryornot python3-binaryornot +binoculars python3-binoculars +binwalk python3-binwalk +bioblend python3-bioblend +bioframe python3-bioframe +biom_format python3-biom-format +biomaj python3-biomaj3 +biomaj_cli python3-biomaj3-cli +biomaj_core python3-biomaj3-core +biomaj_daemon python3-biomaj3-daemon +biomaj_download python3-biomaj3-download +biomaj_process python3-biomaj3-process +biomaj_user python3-biomaj3-user +biomaj_zipkin python3-biomaj3-zipkin +biopython python3-biopython +biotools python3-biotools +bioxtasraw python3-bioxtasraw +bip32utils python3-bip32utils +biplist python3-biplist +bitarray python3-bitarray +bitbucket_api python3-bitbucket-api +bitshuffle bitshuffle +bitstring python3-bitstring +bitstruct python3-bitstruct +bjdata python3-bjdata +black black +bladerf python3-bladerf +blaeu blaeu +blag blag +bleach python3-bleach +bleak python3-bleak +blessed python3-blessed +blessings python3-blessings +blinker python3-blinker +blis python3-cython-blis +blockdiag python3-blockdiag +bloom python3-bloom +blosc python3-blosc +blurhash_python python3-blurhash +bmap_tools bmap-tools +bmtk python3-bmtk +boltons python3-boltons +bondpy python3-bondpy +bonsai python3-bonsai +bookletimposer bookletimposer +boolean.py python3-boolean +booleanOperations python3-booleanoperations +borgbackup borgbackup +borgmatic borgmatic +boto python3-boto +boto3 python3-boto3 +botocore python3-botocore +bottle python3-bottle +bottle_beaker python3-bottle-beaker +bottle_cork python3-bottle-cork +bottle_sqlite python3-bottle-sqlite +bpython bpython +bqplot python3-bqplot +braceexpand python3-braceexpand +bracex python3-bracex +braintree python3-braintree +branca python3-branca +breathe python3-breathe +brebis brebis +breezy python3-breezy +brial python3-brial +brz_debian brz-debian +brz_etckeeper etckeeper +bsddb3 python3-bsddb3 +btchip_python python3-btchip +btest btest +btrfs python3-btrfs +btrfsutil python3-btrfsutil +bugwarrior bugwarrior +build python3-build +buildbot buildbot +buildbot_worker buildbot-worker +buildlog_consultant python3-buildlog-consultant +buku buku +bumblebee_status bumblebee-status +bump2version bumpversion +bumps python3-bumps +bundlewrap bundlewrap +bx_python python3-bx +bytecode python3-bytecode +cached_property python3-cached-property +cachelib python3-cachelib +cachetools python3-cachetools +cachy python3-cachy +cairocffi python3-cairocffi +cajarename caja-rename +caldav python3-caldav +calendarweek python3-calendarweek +calmjs python3-calmjs +calmjs.parse python3-calmjs.parse +calmjs.types python3-calmjs.types +calypso calypso +camera_calibration python3-camera-calibration +camera_calibration_parsers python3-camera-calibration-parsers +canadian_ham_exam canadian-ham-exam +canmatrix python3-canmatrix +canonicaljson python3-canonicaljson +capirca python3-capirca +cappuccino cappuccino +capstone python3-capstone +carbon graphite-carbon +casa_formats_io python3-casa-formats-io +case python3-case +cassandra_driver python3-cassandra +castellan python3-castellan +catalogue python3-catalogue +catfish catfish +catfishq catfishq +catkin python3-catkin +catkin_pkg python3-catkin-pkg +catkin_tools catkin-tools +cattrs python3-cattr +cbor python3-cbor +cbor2 python3-cbor2 +ccdproc python3-ccdproc +cclib python3-cclib +cctbx python3-cctbx +cdiff python3-cdiff +cdist cdist +cdo python3-cdo +cdsapi python3-cdsapi +cecilia cecilia +cedar_backup3 cedar-backup3 +ceilometer python3-ceilometer +ceilometermiddleware python3-ceilometermiddleware +celery python3-celery +celery_haystack_ng python3-celery-haystack-ng +celery_progress python3-celery-progress +ceph python3-ceph-common +ceph_iscsi ceph-iscsi +ceph_volume ceph-osd +cephfs python3-cephfs +cephfs_top cephfs-top +certbot python3-certbot +certbot_apache python3-certbot-apache +certbot_dns_cloudflare python3-certbot-dns-cloudflare +certbot_dns_digitalocean python3-certbot-dns-digitalocean +certbot_dns_dnsimple python3-certbot-dns-dnsimple +certbot_dns_gehirn python3-certbot-dns-gehirn +certbot_dns_google python3-certbot-dns-google +certbot_dns_linode python3-certbot-dns-linode +certbot_dns_ovh python3-certbot-dns-ovh +certbot_dns_rfc2136 python3-certbot-dns-rfc2136 +certbot_dns_route53 python3-certbot-dns-route53 +certbot_dns_sakuracloud python3-certbot-dns-sakuracloud +certbot_dns_standalone python3-certbot-dns-standalone +certbot_nginx python3-certbot-nginx +certbot_plugin_gandi python3-certbot-dns-gandi +certifi python3-certifi +certipy python3-certipy +cffi python3-cffi +cffsubr python3-cffsubr +cfg_diag python3-cfg-diag +cfgrib python3-cfgrib +cfgv python3-cfgv +cftime python3-cftime +cgecore python3-cgecore +changelog python3-changelog +changeo changeo +channels python3-django-channels +channels_redis python3-channels-redis +characteristic python3-characteristic +chardet python3-chardet +chargebee python3-chargebee +charset_normalizer python3-charset-normalizer +chartkick python3-chartkick +chaussette chaussette +check_manifest check-manifest +cheroot python3-cheroot +chirp chirp +ci_info python3-ci-info +cif2cell python3-cif2cell +cigar python3-cigar +cinder python3-cinder +cinder_tempest_plugin cinder-tempest-plugin +circlator circlator +circuitbreaker python3-circuitbreaker +circuits python3-circuits +ciso8601 python3-ciso8601 +citeproc_py python3-citeproc +ck python3-ck +clap_api python3-clap +cleo python3-cleo +clevercsv python3-clevercsv +cli_helpers python3-cli-helpers +cliapp python3-cliapp +click python3-click +click_completion python3-click-completion +click_default_group python3-click-default-group +click_didyoumean python3-click-didyoumean +click_help_colors python3-click-help-colors +click_log python3-click-log +click_man python3-click-man +click_option_group python3-click-option-group +click_plugins python3-click-plugins +click_repl python3-click-repl +click_threading python3-click-threading +clickhouse_driver python3-clickhouse-driver +cliff python3-cliff +cligj python3-cligj +clikit python3-clikit +clint python3-clint +cloud_init cloud-init +cloud_sptheme python3-cloud-sptheme +cloudflare python3-cloudflare +cloudkitty python3-cloudkitty +cloudkitty_dashboard python3-cloudkitty-dashboard +cloudkitty_tempest_plugin cloudkitty-tempest-plugin +cloudpickle python3-cloudpickle +cloup python3-cloup +cluster python3-cluster +cmaes python3-cmaes +cmake_annotate cmake-format +cmake_format cmake-format +cmake_lint cmake-format +cmake_parse cmake-format +cmakelang cmake-format +cmarkgfm python3-cmarkgfm +cmd2 python3-cmd2 +cmdtest cmdtest +cmyt python3-cmyt +coards python3-coards +cobra python3-cobra +codegen python3-codegen +codespell codespell +codicefiscale python3-codicefiscale +cogent3 python3-cogent3 +cognitive_complexity python3-cognitive-complexity +colcon_argcomplete python3-colcon-argcomplete +colcon_bash python3-colcon-bash +colcon_cd python3-colcon-cd +colcon_cmake python3-colcon-cmake +colcon_core python3-colcon-core +colcon_defaults python3-colcon-defaults +colcon_devtools python3-colcon-devtools +colcon_library_path python3-colcon-library-path +colcon_metadata python3-colcon-metadata +colcon_notification python3-colcon-notification +colcon_output python3-colcon-output +colcon_package_information python3-colcon-package-information +colcon_package_selection python3-colcon-package-selection +colcon_parallel_executor python3-colcon-parallel-executor +colcon_pkg_config python3-colcon-pkg-config +colcon_python_setup_py python3-colcon-python-setup-py +colcon_recursive_crawl python3-colcon-recursive-crawl +colcon_ros python3-colcon-ros +colcon_test_result python3-colcon-test-result +colcon_zsh python3-colcon-zsh +colorama python3-colorama +colorcet python3-colorcet +colorclass python3-colorclass +colored python3-colored +colored_traceback python3-colored-traceback +coloredlogs python3-coloredlogs +colorlog python3-colorlog +colormap python3-colormap +colormath python3-colormath +colorspacious python3-colorspacious +colorzero python3-colorzero +colour python3-colour +comm python3-comm +commando python3-commando +commentjson python3-commentjson +commonmark python3-commonmark +compreffor python3-compreffor +compyle python3-compyle +concurrent_log_handler python3-concurrent-log-handler +conda_package_handling conda-package-handling +conda_package_streaming python3-conda-package-streaming +confget python3-confget +configobj python3-configobj +configshell_fb python3-configshell-fb +confluent_kafka python3-confluent-kafka +confusable_homoglyphs python3-confusable-homoglyphs +confuse python3-confuse +congruity congruity +connection_pool python3-connection-pool +consonance python3-consonance +constantly python3-constantly +constraint_grammar python3-cg3 +construct python3-construct +construct_legacy python3-construct.legacy +contextlib2 python3-contextlib2 +contourpy python3-contourpy +convertdate python3-convertdate +cookiecutter python3-cookiecutter +cookies python3-cookies +cooler python3-cooler +coreapi python3-coreapi +coreschema python3-coreschema +cotyledon python3-cotyledon +coverage python3-coverage +coverage_test_runner python3-coverage-test-runner +cplay_ng cplay-ng +cppimport python3-cppimport +cpplint cpplint +cppman cppman +cppy python3-cppy +cptrace python3-ptrace +cpuset python3-cpuset +cracklib python3-cracklib +cram python3-cram +crank python3-crank +crashtest python3-crashtest +crayons python3-crayons +crc32c python3-crc32c +crccheck python3-crccheck +crcelk python3-crcelk +crcmod python3-crcmod +createrepo_c python3-createrepo-c +crispy_forms_foundation python3-django-crispy-forms-foundation +crit criu +crmsh crmsh +crochet python3-crochet +croniter python3-croniter +crossbar python3-crossbar +crossrefapi python3-crossrefapi +crudini crudini +cryptography python3-cryptography +cryptography_vectors python3-cryptography-vectors +cs python3-cs +csa python3-csa +csaps python3-csaps +csb python3-csb +csb43 python3-csb43 +cson python3-cson +css_parser python3-css-parser +csscompressor python3-csscompressor +cssmin python3-cssmin +cssselect python3-cssselect +cssselect2 python3-cssselect2 +cssutils python3-cssutils +csvkit python3-csvkit +ctdconverter ctdconverter +cumin cumin +cups_of_caffeine caffeine +cupshelpers python3-cupshelpers +cursive python3-cursive +curtsies python3-curtsies +custodia python3-custodia +customidenticon python3-customidenticon +cutadapt python3-cutadapt +cuteSV cutesv +cv_bridge python3-cv-bridge +cvdupdate clamav-cvdupdate +cvelib python3-cvelib +cvxopt python3-cvxopt +cwcwidth python3-cwcwidth +cwiid python3-cwiid +cwl_upgrader cwl-upgrader +cwl_utils python3-cwl-utils +cwlformat cwlformat +cwltool cwltool +cyarray python3-cyarray +cycle cycle +cycler python3-cycler +cylc_flow python3-cylc +cymem python3-cymem +cymruwhois python3-cymruwhois +cypari2 python3-cypari2 +cytoolz python3-cytoolz +cyvcf2 python3-cyvcf2 +d2to1 python3-d2to1 +dacite python3-dacite +daemonize python3-daemonize +daiquiri python3-daiquiri +daphne python3-daphne +darkslide darkslide +darts.util.lru python3-darts.lib.utils.lru +dasbus python3-dasbus +dask python3-dask +dask_sphinx_theme python3-dask-sphinx-theme +databases python3-databases +datacache python3-datacache +dataclasses_json python3-dataclasses-json +datalad python3-datalad +datalad_container datalad-container +dateparser python3-dateparser +datrie python3-datrie +db2twitter db2twitter +dbf python3-dbf +dbfread python3-dbfread +dblatex dblatex +dbus_deviation python3-dbusdeviation +dbus_fast python3-dbus-fast +dbus_next python3-dbus-next +dbus_python python3-dbus +dcmstack python3-dcmstack +dcos python3-dcos +ddgr ddgr +ddt python3-ddt +ddupdate ddupdate +deap python3-deap +debdate debdate +debdry debdry +debian_crossgrader crossgrader +debiancontributors python3-debiancontributors +debmake debmake +debmutate python3-debmutate +debocker debocker +debspawn debspawn +debtags debtags +debtcollector python3-debtcollector +debugpy python3-debugpy +decorator python3-decorator +deepTools python3-deeptools +deepdiff python3-deepdiff +deepmerge python3-deepmerge +deeptoolsintervals python3-deeptoolsintervals +defcon python3-defcon +defer python3-defer +defusedxml python3-defusedxml +deluge deluge-common +demjson python3-demjson +denss python3-denss +depinfo python3-depinfo +deprecation python3-deprecation +depthcharge_tools depthcharge-tools +derpconf python3-derpconf +designate python3-designate +designate_dashboard python3-designate-dashboard +designate_tempest_plugin designate-tempest-plugin +designate_tlds designate-tlds +devpi_common python3-devpi-common +devscripts devscripts +dfdatetime python3-dfdatetime +dfvfs python3-dfvfs +dfwinreg python3-dfwinreg +dh_cmake dh-cmake +dh_virtualenv dh-virtualenv +dhcpcanon dhcpcanon +dhcpig dhcpig +dhcpy6d dhcpy6d +diagnostic_analysis python3-diagnostic-analysis +diagnostic_common_diagnostics python3-diagnostic-common-diagnostics +diagnostic_updater python3-diagnostic-updater +diagrams python3-diagrams +dials python3-dials +dials_data python3-dials-data +diaspy_api python3-diaspy +dib_utils python3-dib-utils +diceware diceware +dicoclient python3-dicoclient +dicompyler_core python3-dicompylercore +dict2xml python3-dict2xml +dicteval python3-dicteval +dictobj python3-dictobj +dicttoxml python3-dicttoxml +diff_match_patch python3-diff-match-patch +diffoscope diffoscope-minimal +dill python3-dill +dioptas dioptas +dipy python3-dipy +dirhash python3-dirhash +dirq python3-dirq +dirsearch dirsearch +dirspec python3-dirspec +dirty_equals python3-dirty-equals +discodos discodos +discord.py python3-discord +diskcache python3-diskcache +diskimage_builder python3-diskimage-builder +dissononce python3-dissononce +distlib python3-distlib +distorm3 python3-distorm3 +distributed python3-distributed +distro python3-distro +distro_info python3-distro-info +dj_database_url python3-dj-database-url +dj_static python3-dj-static +django_admin_sortable python3-django-adminsortable +django_ajax_selects python3-ajax-select +django_allauth python3-django-allauth +django_analytical python3-django-analytical +django_any_js python3-django-any-js +django_anymail python3-django-anymail +django_appconf python3-django-appconf +django_assets python3-django-assets +django_auth_ldap python3-django-auth-ldap +django_auto_one_to_one python3-django-auto-one-to-one +django_axes python3-django-axes +django_babel python3-django-babel +django_bitfield python3-django-bitfield +django_bleach python3-django-bleach +django_bootstrap_form python3-django-bootstrapform +django_braces python3-django-braces +django_cachalot python3-django-cachalot +django_cache_machine python3-django-cache-machine +django_cache_memoize python3-django-cache-memoize +django_cacheops python3-django-cacheops +django_cas_client python3-django-casclient +django_cas_server python3-django-cas-server +django_celery_beat python3-django-celery-beat +django_celery_email python3-django-celery-email +django_celery_results python3-django-celery-results +django_ckeditor python3-django-ckeditor +django_classy_tags python3-django-classy-tags +django_cleanup python3-django-cleanup +django_colorfield python3-django-colorfield +django_compressor python3-django-compressor +django_constance python3-django-constance +django_contact_form python3-django-contact-form +django_contrib_comments python3-django-contrib-comments +django_cors_headers python3-django-cors-headers +django_countries python3-django-countries +django_crispy_forms python3-django-crispy-forms +django_csp python3-django-csp +django_cte python3-django-cte +django_dbbackup python3-django-dbbackup +django_dbconn_retry python3-django-dbconn-retry +django_debreach python3-django-debreach +django_debug_toolbar python3-django-debug-toolbar +django_dirtyfields python3-django-dirtyfields +django_downloadview python3-django-downloadview +django_dynamic_preferences python3-django-dynamic-preferences +django_environ python3-django-environ +django_etcd_settings python3-django-etcd-settings +django_extensions python3-django-extensions +django_extra_views python3-django-extra-views +django_favicon_plus_reloaded python3-django-favicon-plus-reloaded +django_filter python3-django-filters +django_formtools python3-django-formtools +django_fsm python3-django-fsm +django_fsm_admin python3-django-fsm-admin +django_graphiql_debug_toolbar python3-django-graphiql-debug-toolbar +django_gravatar2 python3-django-gravatar2 +django_guardian python3-django-guardian +django_haystack python3-django-haystack +django_health_check python3-django-health-check +django_housekeeping python3-django-housekeeping +django_hvad python3-django-hvad +django_ical python3-django-ical +django_iconify python3-django-iconify +django_imagekit python3-django-imagekit +django_impersonate python3-django-impersonate +django_import_export python3-django-import-export +django_invitations python3-django-invitations +django_ipware python3-django-ipware +django_jinja python3-django-jinja +django_js_asset python3-django-js-asset +django_js_reverse python3-django-js-reverse +django_ldapdb python3-django-ldapdb +django_libsass python3-django-libsass +django_macaddress python3-django-macaddress +django_mailman3 python3-django-mailman3 +django_maintenance_mode python3-django-maintenance-mode +django_maintenancemode python3-django-maintenancemode +django_markupfield python3-django-markupfield +django_measurement python3-django-measurement +django_memoize python3-django-memoize +django_menu_generator_ng python3-django-menu-generator-ng +django_model_utils python3-django-model-utils +django_modelcluster python3-django-modelcluster +django_modeltranslation python3-django-modeltranslation +django_mptt python3-django-mptt +django_navtag python3-django-navtag +django_netfields python3-django-netfields +django_nose python3-django-nose +django_notification python3-django-notification +django_oauth_toolkit python3-django-oauth-toolkit +django_ordered_model python3-django-ordered-model +django_organizations python3-django-organizations +django_otp python3-django-otp +django_otp_yubikey python3-django-otp-yubikey +django_pagination python3-django-pagination +django_paintstore python3-django-paintstore +django_parler python3-django-parler +django_pglocks python3-django-pglocks +django_pgschemas python3-django-pgschemas +django_phonenumber_field python3-django-phonenumber-field +django_picklefield python3-django-picklefield +django_pint python3-django-pint +django_pipeline python3-django-pipeline +django_polymodels python3-django-polymodels +django_polymorphic python3-django-polymorphic +django_postgres_extra python3-django-postgres-extra +django_prometheus python3-django-prometheus +django_push_notifications python3-django-push-notifications +django_pyscss python3-django-pyscss +django_python3_ldap python3-django-python3-ldap +django_q python3-django-q +django_qr_code python3-django-qr-code +django_ranged_response python3-django-ranged-response +django_recurrence python3-django-recurrence +django_redis python3-django-redis +django_redis_sessions python3-django-redis-sessions +django_registration python3-django-registration +django_render_block python3-django-render-block +django_rest_hooks python3-django-rest-hooks +django_reversion python3-django-reversion +django_rich python3-django-rich +django_rq python3-django-rq +django_sass python3-django-sass +django_sass_processor python3-django-sass-processor +django_sekizai python3-django-sekizai +django_select2 python3-django-select2 +django_session_security python3-django-session-security +django_setuptest python3-django-setuptest +django_shortuuidfield python3-django-shortuuidfield +django_simple_captcha python3-django-captcha +django_simple_history python3-django-simple-history +django_simple_redis_admin python3-django-redis-admin +django_sitetree python3-django-sitetree +django_sortedm2m python3-sortedm2m +django_split_settings python3-django-split-settings +django_storages python3-django-storages +django_stronghold python3-django-stronghold +django_tables2 python3-django-tables2 +django_tagging python3-django-tagging +django_taggit python3-django-taggit +django_tastypie python3-django-tastypie +django_templated_email python3-django-templated-email +django_testproject python3-django-testproject +django_timezone_field python3-django-timezone-field +django_titofisto python3-django-titofisto +django_treebeard python3-django-treebeard +django_uwsgi_ng python3-django-uwsgi-ng +django_waffle python3-django-waffle +django_webpack_loader python3-django-webpack-loader +django_widget_tweaks python3-django-widget-tweaks +django_wkhtmltopdf python3-django-wkhtmltopdf +django_xmlrpc python3-django-xmlrpc +django_yarnpkg python3-django-yarnpkg +djangorestframework python3-djangorestframework +djangorestframework_api_key python3-djangorestframework-api-key +djangorestframework_filters python3-djangorestframework-filters +djangorestframework_gis python3-djangorestframework-gis +djangorestframework_guardian python3-django-restframework-guardian +djangorestframework_simplejwt python3-djangorestframework-simplejwt +djangosaml2 python3-django-saml2 +djoser python3-djoser +djvubind djvubind +dkimpy python3-dkim +dkimpy_milter dkimpy-milter +dlt python3-dlt +dltlyse python3-dltlyse +dmsh python3-dmsh +dna_jellyfish python3-dna-jellyfish +dnaio python3-dnaio +dnarrange dnarrange +dns_lexicon python3-lexicon +dnsdiag dnsdiag +dnslib python3-dnslib +dnspython python3-dnspython +dnsq python3-dnsq +dnstwist dnstwist +dnsviz dnsviz +doc8 python3-doc8 +docformatter python3-docformatter +docker python3-docker +docker_compose docker-compose +docker_pycreds python3-dockerpycreds +dockerpty python3-dockerpty +docopt python3-docopt +docstring_to_markdown python3-docstring-to-markdown +docutils python3-docutils +docxcompose python3-docxcompose +docxtpl python3-docxtpl +dodgy dodgy +dogpile.cache python3-dogpile.cache +dogtail python3-dogtail +doit python3-doit +dolfinx_mpc python3-dolfinx-mpc +domain2idna python3-domain2idna +domain_coordinator python3-domain-coordinator +dominate python3-dominate +donfig python3-donfig +dosage dosage +dot2tex dot2tex +dotenv_cli python3-dotenv-cli +dotty_dict python3-dotty-dict +doxypypy python3-doxypypy +doxyqml doxyqml +dpkt python3-dpkt +dput python3-dput +drf_extensions python3-djangorestframework-extensions +drf_flex_fields python3-djangorestframework-flex-fields +drf_generators python3-djangorestframework-generators +drf_haystack python3-djangorestframework-haystack +drf_spectacular python3-djangorestframework-spectacular +drgn python3-drgn +drizzle python3-drizzle +drmaa python3-drmaa +drms python3-drms +droidlysis droidlysis +dropbox python3-dropbox +drslib python3-drslib +dtcwt python3-dtcwt +dtfabric python3-dtfabric +dtrx dtrx +dtschema dt-schema +duckpy python3-duckpy +duecredit python3-duecredit +dugong python3-dugong +dulwich python3-dulwich +duniterpy python3-duniterpy +duo_client python3-duo-client +duplicity duplicity +dxf2gcode dxf2gcode +dxtbx python3-cctbx +dyda python3-dyda +dynaconf python3-dynaconf +dynamic_reconfigure python3-dynamic-reconfigure +eagerpy python3-eagerpy +easy_ansi python3-easyansi +easydev python3-easydev +easydict python3-easydict +easygui python3-easygui +easysnmp python3-easysnmp +easywebdav python3-easywebdav +eccodes python3-eccodes +ecdsa python3-ecdsa +echo python3-echo +ecmwflibs python3-ecmwflibs +edgegrid_python python3-edgegrid +edlib python3-edlib +efilter python3-efilter +einsteinpy python3-einsteinpy +elastalert elastalert +elasticsearch python3-elasticsearch +elasticsearch_curator python3-elasticsearch-curator +elementpath python3-elementpath +eliot python3-eliot +email_validator python3-email-validator +emcee python3-emcee +emoji python3-emoji +emperor python3-emperor +empy python3-empy +enet python3-enet +enjarify enjarify +enlighten python3-enlighten +enmerkar python3-enmerkar +enrich python3-enrich +entrypoints python3-entrypoints +envisage python3-envisage +envparse python3-envparse +envs python3-envs +enzyme python3-enzyme +epc python3-epc +ephem python3-ephem +ephemeral_port_reserve python3-ephemeral-port-reserve +epigrass epigrass +epimodels python3-epimodels +epoptes epoptes +errbot errbot +escapism python3-escapism +esda python3-esda +esmre python3-esmre +et_xmlfile python3-et-xmlfile +etcd3 python3-etcd3 +etcd3gw python3-etcd3gw +ete3 python3-ete3 +etelemetry python3-etelemetry +etesync python3-etesync +ethtool python3-ethtool +evdev python3-evdev +eventlet python3-eventlet +ewmh python3-ewmh +exabgp python3-exabgp +exam python3-exam +exceptiongroup python3-exceptiongroup +exchangelib python3-exchangelib +execnet python3-execnet +executing python3-executing +exhale python3-exhale +exotel python3-exotel +expiringdict python3-expiringdict +extension_helpers python3-extension-helpers +extinction python3-extinction +extras python3-extras +eyeD3 python3-eyed3 +ezdxf python3-ezdxf +fabio python3-fabio +fabric python3-fabric +fabulous python3-fabulous +factory_boy python3-factory-boy +fades fades +fail2ban fail2ban +faiss python3-faiss +fakeredis python3-fakeredis +fakesleep python3-fakesleep +falcon python3-falcon +fann2 python3-fann2 +fast5 python3-fast5 +fast_histogram python3-fast-histogram +fastapi python3-fastapi +fastbencode python3-fastbencode +fastchunking python3-fastchunking +fastcluster python3-fastcluster +fasteners python3-fasteners +fastentrypoints python3-fastentrypoints +fastimport python3-fastimport +fastjsonschema python3-fastjsonschema +fastkml python3-fastkml +fasttext python3-fasttext +fava python3-fava +fbless fbless +fbtftp python3-fbtftp +fdroidserver fdroidserver +feather_format python3-feather-format +feature_check python3-feature-check +febelfin_coda python3-febelfin-coda +feed2exec feed2exec +feed2toot feed2toot +feedgenerator python3-feedgenerator +feedparser python3-feedparser +fenics_basix python3-basix +fenics_dijitso python3-dijitso +fenics_dolfin python3-dolfin +fenics_ffc python3-ffc +fenics_ffcx python3-ffcx +fenics_fiat python3-fiat +fenics_ufl python3-ufl +fenrir_screenreader fenrir +ffcv python3-ffcv +fhs_paths python3-fhs +fido2 python3-fido2 +fierce fierce +file_encryptor python3-file-encryptor +filelock python3-filelock +filetype python3-filetype +findlibs python3-findlibs +findpython python3-findpython +fire python3-fire +first python3-first +fissix python3-fissix +fisx python3-fisx +fitbit python3-fitbit +fitsio python3-fitsio +fiu python3-fiu +fixtures python3-fixtures +flake8 python3-flake8 +flake8_2020 python3-flake8-2020 +flake8_black python3-flake8-black +flake8_blind_except python3-flake8-blind-except +flake8_builtins python3-flake8-builtins +flake8_class_newline python3-flake8-class-newline +flake8_cognitive_complexity python3-flake8-cognitive-complexity +flake8_comprehensions python3-flake8-comprehensions +flake8_deprecated python3-flake8-deprecated +flake8_docstrings python3-flake8-docstrings +flake8_import_order python3-flake8-import-order +flake8_mutable python3-flake8-mutable +flake8_noqa python3-flake8-noqa +flake8_polyfill python3-flake8-polyfill +flake8_quotes python3-flake8-quotes +flaky python3-flaky +flanker python3-flanker +flasgger python3-flasgger +flask_dance python3-flask-dance +flask_marshmallow python3-flask-marshmallow +flask_mongoengine python3-flask-mongoengine +flask_multistatic python3-flaskext.multistatic +flask_paginate python3-flask-paginate +flask_peewee python3-flask-peewee +flask_talisman python3-flask-talisman +flatbuffers python3-flatbuffers +flatlatex python3-flatlatex +flexmock python3-flexmock +flickrapi python3-flickrapi +flit flit +flit_core flit +flit_scm python3-flit-scm +flox python3-flox +fluent_logger python3-fluent-logger +flufl.bounce python3-flufl.bounce +flufl.enum python3-flufl.enum +flufl.i18n python3-flufl.i18n +flufl.lock python3-flufl.lock +flufl.testing python3-flufl.testing +fluids python3-fluids +fluster_conformance fluster +flye flye +folium python3-folium +fontMath python3-fontmath +fontParts python3-fontparts +fontPens python3-fontpens +fontmake python3-fontmake +fonttools python3-fonttools +foolscap python3-foolscap +formiko formiko +fortls fortran-language-server +fparser python3-fparser +fpylll python3-fpylll +fpyutils python3-fpyutils +freeart python3-freeart +freedom_maker freedom-maker +freenom python3-freenom +freesas python3-freesas +freesasa python3-freesasa +freetype_py python3-freetype +freezegun python3-freezegun +freezer python3-freezer +freezer_api python3-freezer-api +freezer_web_ui python3-freezer-web-ui +frozendict python3-frozendict +frozenlist python3-frozenlist +fs python3-fs +fsspec python3-fsspec +fswrap python3-fswrap +ftputil python3-ftputil +fudge python3-fudge +funcparserlib python3-funcparserlib +funcsigs python3-funcsigs +funcy python3-funcy +furl python3-furl +furo furo +fuse_python python3-fuse +fusepy python3-fusepy +future python3-future +futurist python3-futurist +fuzzywuzzy python3-fuzzywuzzy +fypp fypp +fysom python3-fysom +gTTS python3-gtts +gTTS_token python3-gtts-token +gTranscribe gtranscribe +gWakeOnLAN gwakeonlan +gabbi python3-gabbi +gajim gajim +galileo galileo +gallery_dl gallery-dl +galpy python3-galpy +galternatives galternatives +gammapy python3-gammapy +ganeshactl python3-nfs-ganesha +gast python3-gast +gattlib python3-gattlib +gau2grid python3-gau2grid +gavodachs python3-gavo-utils +gbp git-buildpackage +gbulb python3-gbulb +gcalcli gcalcli +gccjit python3-gccjit +gcircle python3-ferret +gcovr gcovr +gdspy python3-gdspy +gear python3-gear +gencpp python3-gencpp +geneagrapher python3-geneagrapher +geneimpacts python3-geneimpacts +genetic python3-genetic +genlisp python3-genlisp +genmsg python3-genmsg +genpy python3-genpy +gensim python3-gensim +genty python3-genty +geographiclib python3-geographiclib +geoip2 python3-geoip2 +geojson python3-geojson +geolinks python3-geolinks +geomet python3-geomet +geopandas python3-geopandas +geopy python3-geopy +germinate python3-germinate +gerritlib python3-gerritlib +gertty gertty +get_version python3-get-version +getdns python3-getdns +getmail6 getmail6 +gevent python3-gevent +gevent_websocket python3-gevent-websocket +geventhttpclient python3-geventhttpclient +gfal2_util python3-gfal2-util +gfapy python3-gfapy +gffutils python3-gffutils +gflanguages python3-gflanguages +gftools gftools +ghdiff python3-ghdiff +ghp_import ghp-import +gimmik python3-gimmik +ginga python3-ginga +git_big_picture python3-git-big-picture +git_build_recipe git-build-recipe +git_cola git-cola +git_crecord git-crecord +git_delete_merged_branches python3-git-delete-merged-branches +git_filter_repo git-filter-repo +git_imerge git-imerge +git_os_job python3-git-os-job +git_phab git-phab +git_pw git-pw +git_review git-review +git_revise git-revise +gita gita +gitdb python3-gitdb +gitinspector gitinspector +gitlabracadabra gitlabracadabra +gitless gitless +gitlint_core gitlint +gitsome gitsome +gitup python3-git-repo-updater +glad2 python3-glad +glance python3-glance +glance_store python3-glance-store +glance_tempest_plugin glance-tempest-plugin +glcontext python3-glcontext +glean_parser glean-parser +glfw python3-pyglfw +glob2 python3-glob2 +glue glue-sprite +glue_core python3-glue +glyphsLib python3-glyphslib +glyphsets python3-glyphsets +glyphspkg glyphspkg +gmplot python3-gmplot +gmpy2 python3-gmpy2 +gnocchi python3-gnocchi +gnocchiclient python3-gnocchiclient +gnome_activity_journal gnome-activity-journal +gnome_keysign gnome-keysign +gnuplot_py python3-gnuplot +gnuplotlib python3-gnuplotlib +google_api_python_client python3-googleapi +google_auth python3-google-auth +google_auth_httplib2 python3-google-auth-httplib2 +google_auth_oauthlib python3-google-auth-oauthlib +google_i18n_address python3-google-i18n-address +gourmand gourmand +gpapi python3-gpapi +gpaw gpaw +gpfs python3-nfs-ganesha +gpg python3-gpg +gphoto2 python3-gphoto2 +gphoto2_cffi python3-gphoto2cffi +gpiozero python3-gpiozero +gplaycli gplaycli +gpodder gpodder +gps python3-gps +gpsoauth python3-gpsoauth +gpxpy python3-gpxpy +gpxviewer gpxviewer +gpyfft python3-gpyfft +grabserial grabserial +graide graide +gramps gramps +grapefruit python3-grapefruit +graphene python3-graphene +graphene_django python3-django-graphene +graphite2 python3-graphite2 +graphite_api graphite-api +graphite_web graphite-web +graphql_core python3-graphql-core +graphql_relay python3-graphql-relay +graphviz python3-graphviz +graypy python3-graypy +greenlet python3-greenlet +griffe python3-griffe +grokevt grokevt +grpcio python3-grpcio +grpcio_tools python3-grpc-tools +gsd python3-gsd +gssapi python3-gssapi +gsw python3-gsw +gtfparse python3-gtfparse +gtimelog gtimelog +guake guake +gudhi python3-gudhi +guess_language_spirit python3-guess-language +guessit python3-guessit +guidata python3-guidata +guider guider +guiqwt python3-guiqwt +guizero python3-guizero +gumbo python3-gumbo +gunicorn python3-gunicorn +guzzle_sphinx_theme python3-guzzle-sphinx-theme +gvb gvb +gvm_tools gvm-tools +gwcs python3-gwcs +gwebsockets python3-gwebsockets +gyp gyp +h11 python3-h11 +h2 python3-h2 +h5netcdf python3-h5netcdf +h5py._debian_h5py_mpi python3-h5py-mpi +h5py._debian_h5py_serial python3-h5py-serial +h5sparse python3-h5sparse +hachoir hachoir +hacking python3-hacking +halo python3-halo +handy_archives python3-handy-archives +haproxy_log_analysis python3-haproxy-log-analysis +harmony_discord python3-harmony +harmonypy python3-harmonypy +hashID hashid +hashids python3-hashids +hatch_requirements_txt python3-hatch-requirements-txt +hatch_vcs python3-hatch-vcs +hatchling python3-hatchling +haystack_redis python3-django-haystack-redis +hazwaz python3-hazwaz +hcloud python3-hcloud +hdf5plugin python3-hdf5plugin +hdf5storage python3-hdf5storage +hdf_compass python3-hdf-compass +hdmedians python3-hdmedians +hdmf python3-hdmf +headerparser python3-headerparser +healpy python3-healpy +heat_dashboard python3-heat-dashboard +heat_tempest_plugin heat-tempest-plugin +helpdev helpdev +helpman helpman +heudiconv heudiconv +hexbytes python3-hexbytes +hg_git mercurial-git +hgapi python3-hgapi +hickle python3-hickle +hidapi python3-hid +hidapi_cffi python3-hidapi +hiera_py python3-hiera +hinawa_utils python3-hinawa-utils +hips python3-hips +hiredis python3-hiredis +hiro python3-hiro +hkdf python3-hkdf +hl7 python3-hl7 +hnswlib python3-hnswlib +holidays python3-holidays +home_assistant_bluetooth python3-home-assistant-bluetooth +horizon python3-django-horizon +hostsed hostsed +howdoi howdoi +hpack python3-hpack +hsluv python3-hsluv +hsmwiz hsmwiz +html2text python3-html2text +html5_parser python3-html5-parser +html5lib python3-html5lib +html_sanitizer python3-html-sanitizer +html_text python3-html-text +htmlmin python3-htmlmin +httmock python3-httmock +http_parser python3-http-parser +httpbin python3-httpbin +httpcode httpcode +httpcore python3-httpcore +httpie httpie +httplib2 python3-httplib2 +httpretty python3-httpretty +httpsig python3-httpsig +httptools python3-httptools +httpx python3-httpx +hug python3-hug +humanfriendly python3-humanfriendly +humanize python3-humanize +hunspell python3-hunspell +hupper python3-hupper +hurry.filesize python3-hurry.filesize +hvac python3-hvac +hy python3-hy +hydroffice.bag python3-hydroffice.bag +hypercorn python3-hypercorn +hyperframe python3-hyperframe +hyperlink python3-hyperlink +hyperspy python3-hyperspy +hypothesis python3-hypothesis +hypothesis_auto python3-hypothesis-auto +i3ipc python3-i3ipc +i3pystatus i3pystatus +i8c i8c +iapws python3-iapws +ibm_cloud_sdk_core python3-ibm-cloud-sdk-core +ibm_watson python3-ibm-watson +icalendar python3-icalendar +icdiff icdiff +icecream python3-icecream +icmplib python3-icmplib +icoextract python3-icoextract +identify python3-identify +idna python3-idna +idseq_bench idseq-bench +ifaddr python3-ifaddr +igor python3-igor +igraph python3-igraph +ijson python3-ijson +ilorest ilorest +image_geometry python3-image-geometry +imageio python3-imageio +imagesize python3-imagesize +imap_tools python3-imap-tools +imaplib2 python3-imaplib2 +imbalanced_learn python3-imblearn +imediff imediff +imexam python3-imexam +img2pdf python3-img2pdf +imgp imgp +imgviz python3-imgviz +iminuit python3-iminuit +immutabledict python3-immutabledict +impacket python3-impacket +impass impass +importlab python3-importlab +importlib_metadata python3-importlib-metadata +importlib_resources python3-importlib-resources +importmagic python3-importmagic +in_toto in-toto +include_server distcc-pump +incremental python3-incremental +indexed_gzip python3-indexed-gzip +infinity python3-infinity +inflect python3-inflect +inflection python3-inflection +influxdb python3-influxdb +iniconfig python3-iniconfig +inifile python3-inifile +iniparse python3-iniparse +injector python3-injector +inotify python3-inotify +input_remapper python3-inputremapper +installation_birthday installation-birthday +installer python3-installer +instaloader instaloader +intake python3-intake +intbitset python3-intbitset +intelhex python3-intelhex +interactive_markers python3-interactive-markers +internetarchive python3-internetarchive +intervals python3-intervals +intervaltree python3-intervaltree +intervaltree_bio python3-intervaltree-bio +invoke python3-invoke +ionit ionit +iotop iotop +iow python3-iow +iowait python3-iowait +ipaclient python3-ipaclient +ipahealthcheck freeipa-healthcheck +ipalib python3-ipalib +ipaplatform python3-ipalib +ipapython python3-ipalib +ipdb python3-ipdb +ipfix python3-ipfix +ipp python3-libtrace +iptables_converter iptables-converter +ipykernel python3-ipykernel +ipyparallel python3-ipyparallel +ipython python3-ipython +ipython_genutils python3-ipython-genutils +ipywidgets python3-ipywidgets +irc python3-irc +irclog2html irclog2html +iredis iredis +ironic python3-ironic +ironic_inspector python3-ironic-inspector +ironic_lib python3-ironic-lib +ironic_tempest_plugin ironic-tempest-plugin +ironic_ui python3-ironic-ui +isbg isbg +isbnlib python3-isbnlib +isc_dhcp_leases python3-isc-dhcp-leases +iso3166 python3-iso3166 +iso8601 python3-iso8601 +isodate python3-isodate +isort python3-isort +isosurfaces python3-isosurfaces +isoweek python3-isoweek +itango python3-itango +itemadapter python3-itemadapter +itemloaders python3-itemloaders +itsdangerous python3-itsdangerous +itypes python3-itypes +iva iva +j2cli j2cli +janus python3-janus +jaraco.classes python3-jaraco.classes +jaraco.collections python3-jaraco.collections +jaraco.context python3-jaraco.context +jaraco.functools python3-jaraco.functools +jaraco.itertools python3-jaraco.itertools +jaraco.text python3-jaraco.text +javaobj_py3 python3-javaobj +javaproperties python3-javaproperties +jc jc +jdata python3-jdata +jdcal python3-jdcal +jedi python3-jedi +jeepney python3-jeepney +jeepyb jeepyb +jellyfish python3-jellyfish +jenkins_job_builder python3-jenkins-job-builder +jenkinsapi python3-jenkinsapi +jieba python3-jieba +jinja2_time python3-jinja2-time +jinja_vanish python3-jinja-vanish +jira python3-jira +jmespath python3-jmespath +joblib python3-joblib +joint_state_publisher joint-state-publisher +joint_state_publisher_gui joint-state-publisher-gui +josepy python3-josepy +journal_brief journal-brief +joypy python3-joypy +jplephem python3-jplephem +jpy python3-jpy +jpylyzer python3-jpylyzer +jsbeautifier python3-jsbeautifier +jschema_to_python python3-jschema-to-python +jsmin python3-jsmin +json5 python3-json5 +json_rpc python3-jsonrpc +json_tricks python3-json-tricks +jsondiff python3-jsondiff +jsonext python3-jsonext +jsonhyperschema_codec python3-jsonhyperschema-codec +jsonnet python3-jsonnet +jsonpatch python3-jsonpatch +jsonpath_rw python3-jsonpath-rw +jsonpath_rw_ext python3-jsonpath-rw-ext +jsonpickle python3-jsonpickle +jsonpointer python3-json-pointer +jsonrpclib_pelix python3-jsonrpclib-pelix +jsonschema python3-jsonschema +jstyleson python3-jstyleson +junit_xml python3-junit.xml +junitparser python3-junitparser +junitxml python3-junitxml +junos_eznc python3-junos-eznc +jupyter_client python3-jupyter-client +jupyter_console python3-jupyter-console +jupyter_core python3-jupyter-core +jupyter_kernel_test python3-jupyter-kernel-test +jupyter_packaging python3-jupyter-packaging +jupyter_server python3-jupyter-server +jupyter_server_mathjax python3-jupyter-server-mathjax +jupyter_sphinx python3-jupyter-sphinx +jupyter_sphinx_theme python3-jupyter-sphinx-theme +jupyter_telemetry python3-jupyter-telemetry +jupyterhub jupyterhub +jupyterlab_pygments python3-jupyterlab-pygments +jupyterlab_server python3-jupyterlab-server +jwcrypto python3-jwcrypto +kafka_python python3-kafka +kaitaistruct python3-kaitaistruct +kajiki python3-kajiki +kamcli kamcli +kanboard python3-kanboard +kanboard_cli kanboard-cli +kanjidraw python3-kanjidraw +kapidox kapidox +kaptan python3-kaptan +karabo_bridge python3-karabo-bridge +kas kas +kazam kazam +kazoo python3-kazoo +kconfiglib python3-kconfiglib +kdtree python3-kdtree +keepalive python3-keepalive +keyman_config python3-keyman-config +keymapper keymapper +keyring python3-keyring +keyrings.alt python3-keyrings.alt +keystone python3-keystone +keystone_tempest_plugin keystone-tempest-plugin +keystoneauth1 python3-keystoneauth1 +keystonemiddleware python3-keystonemiddleware +keyutils python3-keyutils +kgb python3-kgb +khal khal +khard khard +khmer khmer +kineticsTools python3-kineticstools +kitchen python3-kitchen +kiwi kiwi +kiwisolver python3-kiwisolver +klaus python3-klaus +knack python3-knack +knitpy python3-knitpy +knockpy knockpy +kombu python3-kombu +krop krop +kthresher kthresher +kubernetes python3-kubernetes +kytos_sphinx_theme python3-kytos-sphinx-theme +kytos_utils kytos-utils +l20n python3-l20n +labelme labelme +labgrid python3-labgrid +lamassemble lamassemble +lammps python3-lammps +langdetect python3-langdetect +lark python3-lark +laser_geometry python3-laser-geometry +latexcodec python3-latexcodec +launchpadlib python3-launchpadlib +lava_common lava-common +lava_coordinator lava-coordinator +lava_dispatcher lava-dispatcher +lava_dispatcher_host lava-dispatcher-host +lava_server lava-server +lavacli lavacli +lazr.config python3-lazr.config +lazr.delegates python3-lazr.delegates +lazr.restfulclient python3-lazr.restfulclient +lazr.uri python3-lazr.uri +lazy python3-lazy +lazy_loader python3-lazy-loader +lazy_object_proxy python3-lazy-object-proxy +lazyarray python3-lazyarray +lazygal lazygal +ldap3 python3-ldap3 +ldapdomaindump python3-ldapdomaindump +ldappool python3-ldappool +leather python3-leather +lecm lecm +ledger_autosync ledger-autosync +ledgerhelpers ledgerhelpers +lefse lefse +legacy_api_wrap python3-legacy-api-wrap +legit legit +leidenalg python3-leidenalg +lensfun python3-lensfun +lerc python3-lerc +lesana lesana +lesscpy python3-lesscpy +lfm lfm +liac_arff python3-liac-arff +lib389 python3-lib389 +libais python3-ais +libarchive_c python3-libarchive-c +libcomps python3-libcomps +libconcord python3-libconcord +libconf python3-libconf +libevdev python3-libevdev +libfdt python3-libfdt +libhfst_swig python3-hfst +libi8x python3-libi8x +libkdumpfile python3-libkdumpfile +libknot python3-libknot +liblarch python3-liblarch +libnacl python3-libnacl +libnatpmp python3-libnatpmp +libpysal python3-libpysal +librecaptcha python3-librecaptcha +librouteros python3-librouteros +libsass python3-libsass +libsumo sumo +libthumbor python3-libthumbor +libtmux python3-libtmux +libtorrent python3-libtorrent +libtraci sumo +libusb1 python3-usb1 +libvirt_python python3-libvirt +libzim python3-libzim +license_expression python3-license-expression +lift lift +lightdm_gtk_greeter_settings lightdm-gtk-greeter-settings +limits python3-limits +limnoria limnoria +line_profiler python3-line-profiler +linecache2 python3-linecache2 +linetable python3-linetable +linkify_it_py python3-linkify-it +lintian_brush lintian-brush +linux_show_player linux-show-player +lios lios +liquidctl liquidctl +listparser python3-listparser +litecli litecli +littleutils python3-littleutils +livereload python3-livereload +llfuse python3-llfuse +llvmlite python3-llvmlite +lmdb python3-lmdb +lmfit python3-lmfit +locket python3-locket +lockfile python3-lockfile +locust python3-locust +log_symbols python3-log-symbols +logassert python3-logassert +logfury python3-logfury +loggerhead loggerhead +logging_tree python3-logging-tree +logilab_common python3-logilab-common +logilab_constraint python3-logilab-constraint +loguru python3-loguru +logutils python3-logutils +logzero python3-logzero +londiste python3-londiste +lookatme lookatme +loompy python3-loompy +louis python3-louis +lptools lptools +lqa lqa +lru_dict python3-lru-dict +ltfatpy python3-ltfatpy +lti python3-lti +lttnganalyses python3-lttnganalyses +lttngust python3-lttngust +lttoolbox python3-lttoolbox +luckyLUKS luckyluks +ludev_t ludevit +luma.core python3-luma.core +luma.emulator python3-luma.emulator +luma.lcd python3-luma.lcd +luma.led_matrix python3-luma.led-matrix +luma.oled python3-luma.oled +lunardate python3-lunardate +lunr python3-lunr +lupa python3-lupa +lxml python3-lxml +lybniz lybniz +lz4 python3-lz4 +lz4tools python3-lz4tools +lzss python3-lzss +lzstring python3-lzstring +m2r python3-m2r +m3u8 python3-m3u8 +macaroonbakery python3-macaroonbakery +macaulay2_jupyter_kernel macaulay2-jupyter-kernel +macholib python3-macholib +magcode_core python3-magcode-core +magic_wormhole magic-wormhole +magic_wormhole_mailbox_server python3-magic-wormhole-mailbox-server +magic_wormhole_transit_relay magic-wormhole-transit-relay +magnum python3-magnum +magnum_tempest_plugin magnum-tempest-plugin +magnum_ui python3-magnum-ui +mailer python3-mailer +mailman mailman3 +mailman_hyperkitty python3-mailman-hyperkitty +mailmanclient python3-mailmanclient +mailnag mailnag +maison python3-maison +makefun python3-makefun +mallard_ducktype python3-mallard.ducktype +mando python3-mando +manila python3-manila +manila_tempest_plugin manila-tempest-plugin +manila_ui python3-manila-ui +mantis_xray mantis-xray +manuel python3-manuel +mapbox_earcut python3-mapbox-earcut +mapdamage mapdamage +mapnik python3-mapnik +mappy python3-mappy +mapscript python3-mapscript +marathon python3-marathon +marisa python3-marisa +markdown2 python3-markdown2 +markdown_callouts python3-markdown-callouts +markdown_exec python3-markdown-exec +markdown_include python3-markdown-include +markdown_it_py python3-markdown-it +marshmallow python3-marshmallow +marshmallow_dataclass python3-marshmallow-dataclass +marshmallow_enum python3-marshmallow-enum +marshmallow_polyfield python3-marshmallow-polyfield +marshmallow_sqlalchemy python3-marshmallow-sqlalchemy +masakari python3-masakari +masakari_dashboard python3-masakari-dashboard +masakari_monitors python3-masakari-monitors +mate_hud mate-hud +mate_menu mate-menu +mate_tweak mate-tweak +mathlibtools mathlibtools +matplotlib python3-matplotlib +matplotlib_inline python3-matplotlib-inline +matplotlib_venn python3-matplotlib-venn +matrix_common python3-matrix-common +matrix_nio python3-matrix-nio +matrix_sydent matrix-sydent +matrix_synapse matrix-synapse +matrix_synapse_ldap3 matrix-synapse-ldap3 +maxminddb python3-maxminddb +mayavi mayavi2 +mbed_host_tests python3-mbed-host-tests +mbed_ls python3-mbed-ls +mbstrdecoder python3-mbstrdecoder +mccabe python3-mccabe +md_toc python3-md-toc +mdit_py_plugins python3-mdit-py-plugins +mdtraj python3-mdtraj +mdurl python3-mdurl +measurement python3-measurement +mecab_python python3-mecab +mechanize python3-mechanize +mediafile python3-mediafile +meld3 python3-meld3 +membernator membernator +memoized_property python3-memoized-property +memory_allocator python3-memory-allocator +memory_profiler python3-memory-profiler +memprof python3-memprof +menulibre menulibre +mercurial mercurial-common +mercurial_extension_utils python3-mercurial-extension-utils +mercurial_keyring mercurial-keyring +merge3 python3-merge3 +mergedeep python3-mergedeep +mergedict python3-mergedict +meshio python3-meshio +meshplex python3-meshplex +meshzoo python3-meshzoo +meson meson +meson_python python3-mesonpy +message_filters python3-message-filters +metaconfig python3-metaconfig +metakernel python3-metakernel +metalfinder metalfinder +metastudent metastudent +meteo_qt meteo-qt +metomi_isodatetime python3-isodatetime +metview python3-metview +mf2py python3-mf2py +microversion_parse python3-microversion-parse +mido python3-mido +milksnake python3-milksnake +mimerender python3-mimerender +mini_buildd python3-mini-buildd +mini_dinstall mini-dinstall +mini_soong mini-soong +minidb python3-minidb +minieigen python3-minieigen +minigalaxy minigalaxy +mininet mininet +miniupnpc python3-miniupnpc +mintpy python3-mintpy +mir_eval python3-mir-eval +mirtop python3-mirtop +mistletoe python3-mistletoe +mistral python3-mistral +mistral_dashboard python3-mistral-dashboard +mistral_lib python3-mistral-lib +mistral_tempest_tests mistral-tempest-plugin +mistune python3-mistune +mistune0 python3-mistune0 +mitmproxy mitmproxy +mitogen python3-mitogen +mkautodoc python3-mkautodoc +mkchromecast mkchromecast +mkdocs mkdocs +mkdocs_autorefs mkdocs-autorefs +mkdocs_click mkdocs-click +mkdocs_literate_nav mkdocs-literate-nav +mkdocs_material_extensions mkdocs-material-extensions +mkdocs_redirects mkdocs-redirects +mkdocs_section_index mkdocs-section-index +mkdocstrings mkdocstrings +mkdocstrings_python mkdocstrings-python-handlers +mkdocstrings_python_legacy mkdocstrings-python-legacy +mkosi mkosi +ml_collections python3-ml-collections +mlbstreamer mlbstreamer +mlpack python3-mlpack +mlpy python3-mlpy +mmcif_pdbx python3-pdbx +mmtf_python python3-mmtf +mne python3-mne +mnemonic python3-mnemonic +mock python3-mock +mock_open python3-mock-open +mockito python3-mockito +mockldap python3-mockldap +mockupdb python3-mockupdb +mod_python libapache2-mod-python +model_bakery python3-model-bakery +modem_cmd modem-cmd +moderngl python3-moderngl +moderngl_window python3-moderngl-window +modernize python3-libmodernize +mofapy python3-mofapy +moksha.common python3-moksha.common +molotov python3-molotov +monajat python3-monajat +monasca_statsd python3-monasca-statsd +mongoengine python3-mongoengine +mongomock python3-mongomock +monotonic python3-monotonic +monty python3-monty +more_itertools python3-more-itertools +moreorless python3-moreorless +morph python3-morph +morris python3-morris +motor python3-motor +mousetrap gnome-mousetrap +moviepy python3-moviepy +mox3 python3-mox3 +mozilla_devscripts mozilla-devscripts +mpegdash python3-mpegdash +mpi4py python3-mpi4py +mpi4py_fft python3-mpi4py-fft +mpiplus python3-mpiplus +mpl_animators python3-mpl-animators +mpl_scatter_density python3-mpl-scatter-density +mpl_sphinx_theme python3-mpl-sphinx-theme +mplcursors python3-mplcursors +mplexporter python3-mplexporter +mpmath python3-mpmath +mps_youtube mps-youtube +mrcfile python3-mrcfile +mrtparse python3-mrtparse +msal python3-msal +msal_extensions python3-msal-extensions +msgpack python3-msgpack +msgpack_numpy python3-msgpack-numpy +msmb_theme python3-msmb-theme +msoffcrypto_tool python3-msoffcrypto-tool +msrest python3-msrest +msrestazure python3-msrestazure +mssql_django python3-mssql-django +mugshot mugshot +mujson python3-mujson +multi_key_dict python3-multi-key-dict +multidict python3-multidict +multipledispatch python3-multipledispatch +multipletau python3-multipletau +multiplex python3-multiplex +multiprocess python3-multiprocess +multiqc multiqc +multisplitby python3-multisplitby +munch python3-munch +munkres python3-munkres +murano python3-murano +murano_agent murano-agent +murano_dashboard python3-murano-dashboard +murano_pkg_check python3-murano-pkg-check +murano_tempest_plugin murano-tempest-plugin +murmurhash python3-murmurhash +music python3-music +musicbrainzngs python3-musicbrainzngs +mutagen python3-mutagen +mwclient python3-mwclient +mwoauth python3-mwoauth +mwparserfromhell python3-mwparserfromhell +mycli mycli +mygpoclient python3-mygpoclient +myhdl python3-myhdl +mypy python3-mypy +mypy_extensions python3-mypy-extensions +mypy_protobuf mypy-protobuf +mysql_connector_python python3-mysql.connector +mysqlclient python3-mysqldb +myst_parser python3-myst-parser +nagiosplugin python3-nagiosplugin +nagstamon nagstamon +nala nala +nameparser python3-nameparser +nanoget python3-nanoget +nanomath python3-nanomath +natkit python3-libtrace +natsort python3-natsort +navarp python3-navarp +nb2plots python3-nb2plots +nbclassic python3-nbclassic +nbclient python3-nbclient +nbconvert python3-nbconvert +nbformat python3-nbformat +nbgitpuller python3-nbgitpuller +nbsphinx python3-nbsphinx +nbsphinx_link python3-nbsphinx-link +nbxmpp python3-nbxmpp +ncbi_acc_download ncbi-acc-download +ncclient python3-ncclient +ncls python3-ncls +ndcube python3-ndcube +ndg_httpsclient python3-ndg-httpsclient +neo python3-neo +nest_asyncio python3-nest-asyncio +netCDF4 python3-netcdf4 +netaddr python3-netaddr +netdisco python3-netdisco +netfilter python3-netfilter +netifaces python3-netifaces +netmiko python3-netmiko +network_runner python3-network-runner +network_wrapper python3-network +networking_bagpipe python3-networking-bagpipe +networking_baremetal python3-ironic-neutron-agent +networking_bgpvpn python3-networking-bgpvpn +networking_l2gw python3-networking-l2gw +networking_mlnx python3-networking-mlnx +networking_sfc python3-networking-sfc +networkx python3-networkx +neutron python3-neutron +neutron_dynamic_routing python3-neutron-dynamic-routing +neutron_ha_tool neutron-ha-tool +neutron_lib python3-neutron-lib +neutron_tempest_plugin neutron-tempest-plugin +neutron_vpnaas python3-neutron-vpnaas +neutron_vpnaas_dashboard python3-neutron-vpnaas-dashboard +nextstrain_augur augur +nftables python3-nftables +ngs python3-ngs +nibabel python3-nibabel +nine python3-nine +nipy python3-nipy +nipype python3-nipype +nitime python3-nitime +nltk python3-nltk +nml nml +nodeenv nodeenv +noise python3-noise +nordugrid_arc_nagios_plugins nordugrid-arc-nagios-plugins +nose python3-nose +nose2 python3-nose2 +noseOfYeti python3-noseofyeti +nose_exclude python3-nose-exclude +nose_parameterized python3-nose-parameterized +nose_random python3-nose-random +nose_timer python3-nose-timer +nosehtmloutput python3-nosehtmloutput +nosexcover python3-nosexcover +notcurses python3-notcurses +notebook python3-notebook +notify2 python3-notify2 +notmuch python3-notmuch +notmuch2 python3-notmuch2 +notofonttools python3-nototools +notus_scanner notus-scanner +nova python3-nova +nox python3-nox +npm2deb npm2deb +npx python3-npx +nrpe_ng nrpe-ng +nsscache nsscache +ntlm_auth python3-ntlm-auth +ntp python3-ntp +ntplib python3-ntplib +nudatus python3-nudatus +num2words python3-num2words +numba python3-numba +numcodecs python3-numcodecs +numexpr python3-numexpr +numpy python3-numpy +numpy_groupies python3-numpy-groupies +numpy_stl python3-stl +numpydoc python3-numpydoc +numpysane python3-numpysane +nvchecker nvchecker +nwdiag python3-nwdiag +nyx nyx +oauth2client python3-oauth2client +oauthlib python3-oauthlib +objgraph python3-objgraph +obsub python3-obsub +ocrmypdf ocrmypdf +octave_kernel python3-octave-kernel +octavia python3-octavia +octavia_dashboard python3-octavia-dashboard +octavia_lib python3-octavia-lib +octavia_tempest_plugin octavia-tempest-plugin +odfpy python3-odf +odoo odoo-14 +offtrac python3-offtrac +ofxclient python3-ofxclient +ofxhome python3-ofxhome +ofxparse python3-ofxparse +ofxstatement ofxstatement +ofxstatement_airbankcz ofxstatement-plugins +ofxstatement_al_bank ofxstatement-plugins +ofxstatement_austrian ofxstatement-plugins +ofxstatement_be_argenta ofxstatement-plugins +ofxstatement_be_ing ofxstatement-plugins +ofxstatement_be_kbc ofxstatement-plugins +ofxstatement_be_keytrade ofxstatement-plugins +ofxstatement_be_triodos ofxstatement-plugins +ofxstatement_betterment ofxstatement-plugins +ofxstatement_bubbas ofxstatement-plugins +ofxstatement_consors ofxstatement-plugins +ofxstatement_czech ofxstatement-plugins +ofxstatement_dab ofxstatement-plugins +ofxstatement_de_ing ofxstatement-plugins +ofxstatement_de_triodos ofxstatement-plugins +ofxstatement_fineco ofxstatement-plugins +ofxstatement_germany_1822direkt ofxstatement-plugins +ofxstatement_germany_postbank ofxstatement-plugins +ofxstatement_intesasp ofxstatement-plugins +ofxstatement_is_arionbanki ofxstatement-plugins +ofxstatement_iso20022 ofxstatement-plugins +ofxstatement_lansforsakringar ofxstatement-plugins +ofxstatement_latvian ofxstatement-plugins +ofxstatement_lfs ofxstatement-plugins +ofxstatement_lithuanian ofxstatement-plugins +ofxstatement_mbank.sk ofxstatement-plugins +ofxstatement_otp ofxstatement-plugins +ofxstatement_polish ofxstatement-plugins +ofxstatement_postfinance ofxstatement-plugins +ofxstatement_raiffeisencz ofxstatement-plugins +ofxstatement_russian ofxstatement-plugins +ofxstatement_seb ofxstatement-plugins +ofxstatement_simple ofxstatement-plugins +ofxstatement_unicreditcz ofxstatement-plugins +ognibuild ognibuild +olefile python3-olefile +omegaconf python3-omegaconf +omemo_backend_signal python3-omemo-backend-signal +omgifol python3-omg +onboard onboard +onedrivesdk python3-onedrivesdk +onetimepass python3-onetimepass +onewire python3-onewire +onioncircuits onioncircuits +onionprobe onionprobe +onionshare onionshare +onionshare_cli onionshare-cli +onnx python3-onnx +ont_fast5_api ont-fast5-api +ont_tombo tombo +ontospy python3-ontospy +ooolib_python python3-ooolib +opcodes python3-opcodes +open3d python3-open3d +openEMS python3-openems +openMotor openmotor +openTSNE python3-opentsne +opendrop opendrop +openidc_client python3-python-openidc-client +openpaperwork_core openpaperwork-core +openpaperwork_gtk openpaperwork-gtk +openpyxl python3-openpyxl +openqa_client python3-openqa-client +openscap_daemon openscap-daemon +openshift python3-openshift +openshot_qt openshot-qt +openslide_python python3-openslide +openstack.nose_plugin python3-openstack.nose-plugin +openstack_heat python3-heat +openstack_placement python3-placement +openstackdocstheme python3-openstackdocstheme +openstacksdk python3-openstacksdk +openstep_plist python3-openstep-plist +opentimestamps python3-opentimestamps +opentracing python3-opentracing +openturns python3-openturns +opentype_sanitizer python3-ots +opgpcard opgpcard +optlang python3-optlang +optuna python3-optuna +oracledb python3-oracledb +orbit_predictor python3-orbit-predictor +ordered_set python3-ordered-set +orderedattrdict python3-orderedattrdict +orderedmultidict python3-orderedmultidict +orderedset python3-orderedset +organize_tool organize +ormar python3-ormar +ortools python3-ortools +os_api_ref python3-os-api-ref +os_apply_config python3-os-apply-config +os_brick python3-os-brick +os_client_config python3-os-client-config +os_collect_config python3-os-collect-config +os_faults python3-os-faults +os_ken python3-os-ken +os_refresh_config python3-os-refresh-config +os_resource_classes python3-os-resource-classes +os_service_types python3-os-service-types +os_testr python3-os-testr +os_traits python3-os-traits +os_vif python3-os-vif +os_win python3-os-win +os_xenapi python3-os-xenapi +osc osc +osc_lib python3-osc-lib +osc_placement python3-osc-placement +osc_plugin_dput osc-plugin-dput +oscrypto python3-oscrypto +oslo.cache python3-oslo.cache +oslo.concurrency python3-oslo.concurrency +oslo.config python3-oslo.config +oslo.context python3-oslo.context +oslo.db python3-oslo.db +oslo.i18n python3-oslo.i18n +oslo.limit python3-oslo.limit +oslo.log python3-oslo.log +oslo.messaging python3-oslo.messaging +oslo.metrics python3-oslo.metrics +oslo.middleware python3-oslo.middleware +oslo.policy python3-oslo.policy +oslo.privsep python3-oslo.privsep +oslo.reports python3-oslo.reports +oslo.rootwrap python3-oslo.rootwrap +oslo.serialization python3-oslo.serialization +oslo.service python3-oslo.service +oslo.upgradecheck python3-oslo.upgradecheck +oslo.utils python3-oslo.utils +oslo.versionedobjects python3-oslo.versionedobjects +oslo.vmware python3-oslo.vmware +oslosphinx python3-oslosphinx +oslotest python3-oslotest +osmapi python3-osmapi +osmium python3-pyosmium +osmnx python3-osmnx +ospd_openvas ospd-openvas +osprofiler python3-osprofiler +ospurge python3-ospurge +osrf_pycommon python3-osrf-pycommon +ostree_push ostree-push +outcome python3-outcome +overpass python3-overpass +overpy python3-overpy +ovn_octavia_provider python3-ovn-octavia-provider +ovs python3-openvswitch +ovsdbapp python3-ovsdbapp +oz oz +packaging python3-packaging +pacparser python3-pacparser +padaos python3-padaos +padme python3-padme +pafy python3-pafy +pagekite pagekite +pager python3-pager +pagure pagure +paho_mqtt python3-paho-mqtt +pairtools python3-pairtools +paleomix paleomix +palettable python3-palettable +pamela python3-pamela +pamqp python3-pamqp +pandas python3-pandas +pandoc_plantuml_filter pandoc-plantuml-filter +pandocfilters python3-pandocfilters +pangoLEARN python3-pangolearn +pankoclient python3-pankoclient +panoramisk python3-panoramisk +pantalaimon python3-pantalaimon +panwid python3-panwid +paperwork paperwork-gtk +paperwork_backend paperwork-backend +paperwork_shell paperwork-shell +paq python3-paq +parallax python3-parallax +parallel_fastq_dump parallel-fastq-dump +param python3-param +parameterized python3-parameterized +paramiko python3-paramiko +parasail python3-parasail +parfive python3-parfive +parse python3-parse +parse_type python3-parse-type +parsedatetime python3-parsedatetime +parsel python3-parsel +parsero parsero +parsimonious python3-parsimonious +parso python3-parso +partd python3-partd +pass_audit pass-extension-audit +pass_git_helper pass-git-helper +passlib python3-passlib +pastel python3-pastel +patator patator +patatt python3-patatt +patch_ng python3-patch-ng +path python3-path +path_and_address python3-path-and-address +pathspec python3-pathspec +pathspider pathspider +pathtools python3-pathtools +pathvalidate python3-pathvalidate +patiencediff python3-patiencediff +patool patool +patroni patroni +patsy python3-patsy +pauvre python3-pauvre +paypal python3-paypal +pbcommand python3-pbcommand +pbcore python3-pbcore +pbkdf2 python3-pbkdf2 +pbr python3-pbr +pcapy python3-pcapy +pcbasic python3-pcbasic +pcp python3-pcp +pcs pcs +pdb2pqr python3-pdb2pqr +pdb_tools python3-pdbtools +pdbfixer python3-pdbfixer +pdd pdd +pdfarranger pdfarranger +pdfkit python3-pdfkit +pdfminer.six python3-pdfminer +pdfrw python3-pdfrw +pdm python3-pdm +pdm_pep517 python3-pdm-pep517 +pdudaemon pdudaemon +pecan python3-pecan +peewee python3-peewee +pefile python3-pefile +pelican pelican +pem python3-pem +pendulum python3-pendulum +pep517 python3-pep517 +pep8 python3-pep8 +pep8_naming python3-pep8-naming +percol percol +perf linux-perf +periodictable python3-periodictable +persepolis persepolis +persist_queue python3-persist-queue +persistent python3-persistent +persisting_theory python3-persisting-theory +pex python3-pex +pexpect python3-pexpect +pg8000 python3-pg8000 +pg_activity pg-activity +pgbouncer python3-pgbouncer +pgcli pgcli +pglast python3-pglast +pglistener pglistener +pgmagick python3-pgmagick +pgpdump python3-pgpdump +pgq python3-pgq +pgspecial python3-pgspecial +pgxnclient pgxnclient +pgzero python3-pgzero +phabricator python3-phabricator +phat python3-phat +phonenumbers python3-phonenumbers +phonopy python3-phonopy +photocollage photocollage +photofilmstrip photofilmstrip +photutils python3-photutils +phply python3-phply +phpserialize python3-phpserialize +phx_class_registry python3-phx-class-registry +phylo_treetime python3-treetime +picklable_itertools python3-picklable-itertools +pickleshare python3-pickleshare +picopore python3-picopore +piexif python3-piexif +pigpio python3-pigpio +pika python3-pika +pikepdf python3-pikepdf +pil python3-pil +pilkit python3-pilkit +pip python3-pip +pip_check_reqs pip-check-reqs +pipdeptree python3-pipdeptree +pipedviewer python3-ferret +pipenv pipenv +pipsi pipsi +pipx pipx +pius pius +pkgconfig python3-pkgconfig +pkginfo python3-pkginfo +pkihealthcheck pki-server +plac python3-plac +plakativ python3-plakativ +planetary_system_stacker planetary-system-stacker +planetfilter planetfilter +plasTeX python3-plastex +plaso python3-plaso +plaster python3-plaster +plaster_pastedeploy python3-plaster-pastedeploy +platformdirs python3-platformdirs +platformio platformio +pldns python3-libtrace +plip plip +plotly python3-plotly +plover plover +plt python3-libtrace +pluggy python3-pluggy +pluginbase python3-pluginbase +plumbum python3-plumbum +ply python3-ply +plyara python3-plyara +plyer python3-plyer +plyvel python3-plyvel +pocketsphinx python3-pocketsphinx +pocsuite3 pocsuite3 +podcastparser python3-podcastparser +podman_compose podman-compose +poetry python3-poetry +poetry_core python3-poetry-core +poezio poezio +pokrok python3-pokrok +poliastro python3-poliastro +polib python3-polib +policyd_rate_limit policyd-rate-limit +polyline python3-polyline +pomegranate python3-pomegranate +pony python3-pony +pooch python3-pooch +pook python3-pook +porechop porechop +poretools poretools +port_for python3-port-for +portalocker python3-portalocker +portend python3-portend +portio python3-portio +portpicker python3-portpicker +postfix_mta_sts_resolver postfix-mta-sts-resolver +postgresfixture python3-postgresfixture +postorius python3-django-postorius +powa_collector powa-collector +power python3-power +powerline_gitstatus python3-powerline-gitstatus +powerline_status python3-powerline +powerline_taskwarrior python3-powerline-taskwarrior +ppft python3-ppft +pplpy python3-ppl +ppmd_cffi python3-ppmd +pprintpp python3-pprintpp +pprofile python3-pprofile +praw python3-praw +prawcore python3-prawcore +pre_commit pre-commit +precis_i18n python3-precis-i18n +prefixed python3-prefixed +preggy python3-preggy +prelude python3-prelude +prelude_correlator prelude-correlator +preludedb python3-preludedb +presentty presentty +presets python3-presets +preshed python3-preshed +presto python3-presto +pretend python3-pretend +prettylog python3-prettylog +prettytable python3-prettytable +prewikka prewikka +primecountpy python3-primecountpy +priority python3-priority +prison python3-prison +pristine_lfs pristine-lfs +proboscis python3-proboscis +procrunner python3-procrunner +prodigy python3-prodigy +profitbricks python3-profitbricks +proglog python3-proglog +progress python3-progress +progressbar python3-progressbar +progressbar2 python3-progressbar2 +project_generator python3-project-generator +project_generator_definitions python3-project-generator-definitions +proliantutils python3-proliantutils +prometheus_client python3-prometheus-client +prometheus_openstack_exporter prometheus-openstack-exporter +prometheus_pgbouncer_exporter prometheus-pgbouncer-exporter +prometheus_xmpp_alerts prometheus-xmpp-alerts +promise python3-promise +prompt_toolkit python3-prompt-toolkit +propka python3-propka +proselint python3-proselint +proteus tryton-proteus +protobix python3-protobix +protobuf python3-protobuf +prov python3-prov +proxmoxer python3-proxmoxer +psautohint python3-psautohint +psd_tools python3-psd-tools +pssh python3-psshlib +psutil python3-psutil +psycogreen python3-psycogreen +psycopg python3-psycopg3 +psycopg2 python3-psycopg2 +psycopg2cffi python3-psycopg2cffi +psycopg_pool python3-psycopg3-pool +ptk python3-ptk +ptpython ptpython +ptyprocess python3-ptyprocess +publicsuffix2 python3-publicsuffix2 +pubpaste pubpaste +pudb python3-pudb +puddletag puddletag +pulseaudio_dlna pulseaudio-dlna +pulsectl python3-pulsectl +pulsemixer pulsemixer +pure_eval python3-pure-eval +pure_sasl python3-pure-sasl +puremagic python3-puremagic +purl python3-purl +pwman3 pwman3 +pwntools python3-pwntools +pwquality python3-pwquality +pxpx px +py python3-py +py2bit python3-py2bit +py3dns python3-dns +py3exiv2 python3-py3exiv2 +py3status py3status +py7zr python3-py7zr +pyBigWig python3-pybigwig +pyClamd python3-pyclamd +pyFAI python3-pyfai +pyFFTW python3-pyfftw +pyFlow python3-pyflow +pyFltk python3-fltk +pyIOSXR python3-pyiosxr +pyNFFT python3-pynfft +pyOpenSSL python3-openssl +pyPEG2 python3-pypeg2 +pyRFC3339 python3-rfc3339 +pyRdfa3 python3-pyrdfa +pySFML python3-sfml +pyScss python3-pyscss +pyVows python3-pyvows +py_cpuinfo python3-cpuinfo +py_enigma python3-enigma +py_lz4framed python3-lz4framed +py_moneyed python3-moneyed +py_postgresql python3-postgresql +py_radix python3-radix +py_stringmatching python3-py-stringmatching +py_ubjson python3-ubjson +py_zipkin python3-py-zipkin +pyabpoa python3-pyabpoa +pyacoustid python3-acoustid +pyaes python3-pyaes +pyagentx python3-pyagentx +pyahocorasick python3-ahocorasick +pyalsa python3-pyalsa +pyalsaaudio python3-alsaaudio +pyaml python3-pretty-yaml +pyani python3-pyani +pyannotate python3-pyannotate +pyao python3-pyao +pyaps3 python3-pyaps3 +pyarmnn python3-pyarmnn +pyasn python3-pyasn +pyasn1 python3-pyasn1 +pyasn1_modules python3-pyasn1-modules +pyassimp python3-pyassimp +pyaxmlparser python3-pyaxmlparser +pybadges python3-pybadges +pybeam python3-pybeam +pybedtools python3-pybedtools +pybel python3-pybel +pybind11 python3-pybind11 +pybtex python3-pybtex +pybtex_docutils python3-pybtex-docutils +pybugz bugz +pycadf python3-pycadf +pycairo python3-cairo +pycallgraph python3-pycallgraph +pycares python3-pycares +pycdlib python3-pycdlib +pychm python3-chm +pychopper python3-pychopper +pycirkuit pycirkuit +pyclipper python3-pyclipper +pyclustering python3-pyclustering +pycoQC pycoqc +pycoast python3-pycoast +pycodcif python3-pycodcif +pycodestyle python3-pycodestyle +pycollada python3-collada +pycosat python3-pycosat +pycountry python3-pycountry +pycparser python3-pycparser +pycryptodomex python3-pycryptodome +pycryptosat python3-cryptominisat +pyct python3-pyct +pycups python3-cups +pycurl python3-pycurl +pydantic python3-pydantic +pydata_sphinx_theme python3-pydata-sphinx-theme +pydbus python3-pydbus +pydecorate python3-pydecorate +pydenticon python3-pydenticon +pydevd python3-pydevd +pydicom python3-pydicom +pydl python3-pydl +pydle python3-pydle +pydocstyle python3-pydocstyle +pydoctor pydoctor +pydot python3-pydot +pydotplus python3-pydotplus +pyds9 python3-pyds9 +pydub python3-pydub +pydyf python3-pydyf +pyeapi python3-pyeapi +pyeclib python3-pyeclib +pyee python3-pyee +pyelftools python3-pyelftools +pyemd python3-pyemd +pyenchant python3-enchant +pyensembl pyensembl +pyepics python3-pyepics +pyepr python3-epr +pyepsg python3-pyepsg +pyequihash python3-equihash +pyerfa python3-erfa +pyethash python3-pyethash +pyface python3-pyface +pyfaidx python3-pyfaidx +pyfakefs python3-pyfakefs +pyfastaq fastaq +pyfastx python3-pyfastx +pyfavicon python3-pyfavicon +pyferret python3-ferret +pyfg python3-pyfg +pyfiglet python3-pyfiglet +pyflakes python3-pyflakes +pyforge python3-forge +pyfribidi python3-pyfribidi +pyftdi python3-ftdi +pyftpdlib python3-pyftpdlib +pyfuse3 python3-pyfuse3 +pygac python3-pygac +pygal python3-pygal +pygalmesh python3-pygalmesh +pygame python3-pygame +pygame_sdl2 python3-pygame-sdl2 +pygccxml python3-pygccxml +pygeoif python3-pygeoif +pygeoip python3-pygeoip +pygerrit2 python3-pygerrit2 +pyghmi python3-pyghmi +pygit2 python3-pygit2 +pyglet python3-pyglet +pyglossary python3-pyglossary +pygmsh python3-pygmsh +pygopherd pygopherd +pygpu python3-pygpu +pygrace python3-pygrace +pygraphviz python3-pygraphviz +pygrib python3-grib +pygtail python3-pygtail +pygtkspellcheck python3-gtkspellcheck +pygtrie python3-pygtrie +pyhamtools python3-pyhamtools +pyhcl python3-pyhcl +pyhdf python3-hdf4 +pyicloud python3-pyicloud +pyimagetool python3-pyimagetool +pyinotify python3-pyinotify +pyjavaproperties python3-pyjavaproperties +pyjks python3-pyjks +pyjokes python3-pyjokes +pykdtree python3-pykdtree +pykeepass python3-pykeepass +pykerberos python3-kerberos +pykka python3-pykka +pykml python3-pykml +pyknon python3-pyknon +pykwalify python3-pykwalify +pylama python3-pylama +pylast python3-pylast +pylatexenc python3-pylatexenc +pylev python3-pylev +pylibacl python3-pylibacl +pylibdmtx python3-pylibdmtx +pyliblo python3-liblo +pylibmc python3-pylibmc +pylibsrtp python3-pylibsrtp +pylibtiff python3-libtiff +pylint pylint +pylint_celery python3-pylint-celery +pylint_common python3-pylint-common +pylint_django python3-pylint-django +pylint_flask python3-pylint-flask +pylint_plugin_utils python3-pylint-plugin-utils +pylint_venv python3-pylint-venv +pyls_isort python3-pylsp-isort +pyls_spyder python3-pyls-spyder +pylsp_mypy python3-pylsp-mypy +pylsp_rope python3-pylsp-rope +pyluach python3-pyluach +pylxd python3-pylxd +pymacaroons python3-pymacaroons +pymad python3-pymad +pymap3d python3-pymap3d +pymatgen python3-pymatgen +pymbar python3-pymbar +pymdown_extensions python3-pymdownx +pymecavideo python3-mecavideo +pymediainfo python3-pymediainfo +pymemcache python3-pymemcache +pymetar python3-pymetar +pymia python3-mia +pymilter python3-milter +pymoc python3-pymoc +pymodbus python3-pymodbus +pymol python3-pymol +pymongo python3-pymongo +pymox python3-mox +pympress pympress +pymssql python3-pymssql +pymummer python3-pymummer +pymzml python3-pymzml +pynag python3-pynag +pynauty python3-pynauty +pynetbox python3-pynetbox +pyngus python3-pyngus +pyninjotiff python3-pyninjotiff +pynmea2 python3-nmea2 +pynndescent python3-pynndescent +pynpoint python3-pynpoint +pynput python3-pynput +pynvim python3-pynvim +pynwb python3-pynwb +pyo python3-pyo +pyocd python3-pyocd +pyocr python3-pyocr +pyodbc python3-pyodbc +pyodc python3-pyodc +pyopencl python3-pyopencl +pyorbital python3-pyorbital +pyorick python3-pyorick +pyotp python3-pyotp +pyp pyp +pypairix python3-pairix +pypandoc python3-pypandoc +pyparallel python3-parallel +pyparsing python3-pyparsing +pyparted python3-parted +pypartpicker python3-pypartpicker +pypass python3-pypass +pypdf2 python3-pypdf2 +pyperclip python3-pyperclip +pyperform python3-pyperform +pyphen python3-pyphen +pypillowfight python3-pypillowfight +pypmix python3-pmix +pypng python3-png +pypowervm python3-pypowervm +pyppd pyppd +pyprof2calltree pyprof2calltree +pyproj python3-pyproj +pyproject_metadata python3-pyproject-metadata +pyprojroot python3-pyprojroot +pypuppetdb python3-pypuppetdb +pypureomapi python3-pypureomapi +pyqi pyqi +pyqt_distutils python3-pyqt-distutils +pyqtgraph python3-pyqtgraph +pyquery python3-pyquery +pyrad python3-pyrad +pyraf python3-pyraf +pyramid python3-pyramid +pyramid_chameleon python3-pyramid-chameleon +pyramid_jinja2 python3-pyramid-jinja2 +pyramid_multiauth python3-pyramid-multiauth +pyramid_tm python3-pyramid-tm +pyramid_zcml python3-pyramid-zcml +pyranges python3-pyranges +pyrcb2 python3-pyrcb2 +pyregfi python3-pyregfi +pyregion python3-pyregion +pyremctl python3-pyremctl +pyresample python3-pyresample +pyrle python3-pyrle +pyroma python3-pyroma +pyroute2 python3-pyroute2 +pyrr python3-pyrr +pyrsistent python3-pyrsistent +pyrundeck python3-pyrundeck +pysam python3-pysam +pysaml2 python3-pysaml2 +pyscard python3-pyscard +pysendfile python3-sendfile +pyserial python3-serial +pyserial_asyncio python3-serial-asyncio +pyshp python3-pyshp +pysmbc python3-smbc +pysmi python3-pysmi +pysnmp python3-pysnmp4 +pysnmp_apps python3-pysnmp4-apps +pysnmp_mibs python3-pysnmp4-mibs +pysodium python3-pysodium +pysolar python3-pysolar +pysolid python3-pysolid +pysolr python3-pysolr +pyspectral python3-pyspectral +pyspike python3-pyspike +pyspoa python3-pyspoa +pysqm python3-pysqm +pysrs python3-srs +pysrt python3-pysrt +pyssim python3-pyssim +pystache python3-pystache +pystemd python3-pystemd +pystray python3-pystray +pysubnettree python3-subnettree +pysurfer python3-surfer +pysvn python3-svn +pyswarms python3-pyswarms +pysword python3-pysword +pysyncobj python3-pysyncobj +pysynphot python3-pysynphot +pytaglib python3-taglib +pytango python3-tango +pyte python3-pyte +pytest python3-pytest +pytest_arraydiff python3-pytest-arraydiff +pytest_astropy python3-pytest-astropy +pytest_astropy_header python3-pytest-astropy-header +pytest_asyncio python3-pytest-asyncio +pytest_bdd python3-pytest-bdd +pytest_benchmark python3-pytest-benchmark +pytest_click python3-pytest-click +pytest_cookies python3-pytest-cookies +pytest_cov python3-pytest-cov +pytest_cython python3-pytest-cython +pytest_datadir python3-pytest-datadir +pytest_dependency python3-pytest-dependency +pytest_django python3-pytest-django +pytest_djangoapp python3-pytest-djangoapp +pytest_doctestplus python3-pytest-doctestplus +pytest_expect python3-pytest-expect +pytest_filter_subpackage python3-pytest-filter-subpackage +pytest_flake8 python3-pytest-flake8 +pytest_flask python3-pytest-flask +pytest_forked python3-pytest-forked +pytest_golden python3-pytest-golden +pytest_helpers_namespace python3-pytest-helpers-namespace +pytest_httpbin python3-pytest-httpbin +pytest_httpserver python3-pytest-httpserver +pytest_instafail python3-pytest-instafail +pytest_lazy_fixture python3-pytest-lazy-fixture +pytest_localserver python3-pytest-localserver +pytest_mock python3-pytest-mock +pytest_mpi python3-pytest-mpi +pytest_mpl python3-pytest-mpl +pytest_multihost python3-pytest-multihost +pytest_openfiles python3-pytest-openfiles +pytest_order python3-pytest-order +pytest_pep8 python3-pytest-pep8 +pytest_pylint python3-pytest-pylint +pytest_qt python3-pytestqt +pytest_random_order python3-pytest-random-order +pytest_regressions python3-pytest-regressions +pytest_remotedata python3-pytest-remotedata +pytest_repeat python3-pytest-repeat +pytest_rerunfailures python3-pytest-rerunfailures +pytest_runner python3-pytest-runner +pytest_salt python3-pytestsalt +pytest_salt_factories python3-saltfactories +pytest_services python3-pytest-services +pytest_skip_markers python3-pytest-skip-markers +pytest_sourceorder python3-pytest-sourceorder +pytest_subtests python3-pytest-subtests +pytest_sugar python3-pytest-sugar +pytest_tempdir python3-pytest-tempdir +pytest_testinfra python3-testinfra +pytest_timeout python3-pytest-timeout +pytest_toolbox python3-pytest-toolbox +pytest_tornado python3-pytest-tornado +pytest_tornasync python3-pytest-tornasync +pytest_twisted python3-pytest-twisted +pytest_vcr python3-pytest-vcr +pytest_xdist python3-pytest-xdist +pytest_xprocess python3-pytest-xprocess +pytest_xvfb python3-pytest-xvfb +python3_discogs_client python3-discogs-client +python3_lxc python3-lxc +python3_openid python3-openid +python3_saml python3-onelogin-saml2 +python_Levenshtein python3-levenshtein +python_aalib python3-aalib +python_apt python3-apt +python_aptly python3-aptly +python_augeas python3-augeas +python_axolotl python3-axolotl +python_axolotl_curve25519 python3-axolotl-curve25519 +python_barbicanclient python3-barbicanclient +python_bidi python3-bidi +python_binary_memcached python3-binary-memcached +python_bitcoinlib python3-bitcoinlib +python_blazarclient python3-blazarclient +python_box python3-box +python_bugzilla python3-bugzilla +python_can python3-can +python_casacore python3-casacore +python_cinderclient python3-cinderclient +python_cloudkittyclient python3-cloudkittyclient +python_community python3-louvain +python_consul python3-consul +python_consul2 python3-consul2 +python_corepywrap python3-corepywrap +python_cpl python3-cpl +python_crontab python3-crontab +python_cyborgclient python3-cyborgclient +python_daemon python3-daemon +python_dateutil python3-dateutil +python_dbusmock python3-dbusmock +python_debian python3-debian +python_debianbts python3-debianbts +python_decouple python3-decouple +python_designateclient python3-designateclient +python_digitalocean python3-digitalocean +python_distutils_extra python3-distutils-extra +python_djvulibre python3-djvu +python_docs_theme python3-docs-theme +python_docx python3-docx +python_dotenv python3-dotenv +python_dracclient python3-dracclient +python_editor python3-editor +python_engineio python3-engineio +python_espeak python3-espeak +python_etcd python3-etcd +python_evtx python3-evtx +python_fedora python3-fedora +python_freecontact python3-freecontact +python_freezerclient python3-freezerclient +python_gammu python3-gammu +python_geotiepoints python3-geotiepoints +python_gflags python3-gflags +python_gitlab python3-gitlab +python_glanceclient python3-glanceclient +python_glareclient python3-glareclient +python_gnupg python3-gnupg +python_gvm python3-gvm +python_heatclient python3-heatclient +python_hglib python3-hglib +python_hpilo python3-hpilo +python_ilorest_library python3-ilorest +python_instagram python3-instagram +python_ipmi python3-pyipmi +python_iptables python3-iptables +python_irodsclient python3-irodsclient +python_ironicclient python3-ironicclient +python_jenkins python3-jenkins +python_jose python3-jose +python_json_logger python3-pythonjsonlogger +python_k8sclient python3-k8sclient +python_karborclient python3-karborclient +python_keystoneclient python3-keystoneclient +python_ldap python3-ldap +python_libdiscid python3-libdiscid +python_libguess python3-libguess +python_libnmap python3-libnmap +python_librtmp python3-librtmp +python_linux_procfs python3-linux-procfs +python_lsp_black python3-pylsp-black +python_lsp_jsonrpc python3-pylsp-jsonrpc +python_lsp_server python3-pylsp +python_ly python3-ly +python_lzo python3-lzo +python_magic python3-magic +python_magnumclient python3-magnumclient +python_manilaclient python3-manilaclient +python_markdown_math python3-mdx-math +python_masakariclient python3-masakariclient +python_memcached python3-memcache +python_miio python3-miio +python_mimeparse python3-mimeparse +python_mistralclient python3-mistralclient +python_monascaclient python3-monascaclient +python_mpd2 python3-mpd +python_mpv python3-mpv +python_multipart python3-multipart +python_muranoclient python3-muranoclient +python_musicpd python3-musicpd +python_networkmanager python3-networkmanager +python_neutronclient python3-neutronclient +python_nmap python3-nmap +python_novaclient python3-novaclient +python_novnc python3-novnc +python_nubia python3-nubia +python_octaviaclient python3-octaviaclient +python_olm python3-olm +python_openflow python3-openflow +python_openid_cla python3-openid-cla +python_openid_teams python3-openid-teams +python_openstackclient python3-openstackclient +python_pam python3-pampy +python_pcl python3-pcl +python_pcre python3-pcre +python_periphery python3-periphery +python_popcon python3-popcon +python_poppler_qt5 python3-poppler-qt5 +python_potr python3-potr +python_prctl python3-prctl +python_pskc python3-pskc +python_ptrace python3-ptrace +python_qinlingclient python3-qinlingclient +python_qt_binding python3-python-qt-binding +python_rapidjson python3-rapidjson +python_redmine python3-redminelib +python_rtmidi python3-rtmidi +python_saharaclient python3-saharaclient +python_sane python3-sane +python_scciclient python3-scciclient +python_seamicroclient python3-seamicroclient +python_searchlightclient python3-searchlightclient +python_semantic_release python3-semantic-release +python_senlinclient python3-senlinclient +python_slugify python3-slugify +python_snappy python3-snappy +python_socketio python3-socketio +python_socks python3-python-socks +python_sql python3-sql +python_stdnum python3-stdnum +python_subunit python3-subunit +python_svipc python3-svipc +python_swiftclient python3-swiftclient +python_tackerclient python3-tackerclient +python_tds python3-tds +python_telegram_bot python3-python-telegram-bot +python_tempestconf python3-tempestconf +python_termstyle python3-termstyle +python_tr python3-tr +python_troveclient python3-troveclient +python_twitter python3-twitter +python_u2flib_server python3-u2flib-server +python_uinput python3-uinput +python_utils python3-python-utils +python_vagrant python3-vagrant +python_vitrageclient python3-vitrageclient +python_vlc python3-vlc +python_watcher python3-watcher +python_watcherclient python3-watcherclient +python_xlib python3-xlib +python_yubico python3-yubico +python_zaqarclient python3-zaqarclient +python_zunclient python3-zunclient +pythondialog python3-dialog +pythran python3-pythran +pytidylib python3-tidylib +pytimeparse python3-pytimeparse +pytkdocs python3-pytkdocs +pytoml python3-pytoml +pytoolconfig python3-pytoolconfig +pytools python3-pytools +pytorch_ignite python3-torch-ignite +pytrainer pytrainer +pytray python3-pytray +pytroll_schedule python3-trollsched +pytsk3 python3-tsk +pytz python3-tz +pytz_deprecation_shim python3-pytz-deprecation-shim +pytzdata python3-pytzdata +pyuca python3-pyuca +pyudev python3-pyudev +pyupgrade pyupgrade +pyusb python3-usb +pyvmomi python3-pyvmomi +pyvo python3-pyvo +pywatchman python3-pywatchman +pywebview python3-webview +pywinrm python3-winrm +pywps python3-pywps +pywws python3-pywws +pyxDamerauLevenshtein python3-pyxdameraulevenshtein +pyxattr python3-pyxattr +pyxdg python3-xdg +pyxid python3-pyxid +pyxnat python3-pyxnat +pyxs python3-pyxs +pyyaml_env_tag python3-pyyaml-env-tag +pyzabbix python3-pyzabbix +pyzbar python3-pyzbar +pyzmq python3-zmq +pyzor pyzor +q python3-q +q2_alignment q2-alignment +q2_cutadapt q2-cutadapt +q2_dada2 q2-dada2 +q2_demux q2-demux +q2_diversity_lib q2-diversity-lib +q2_emperor q2-emperor +q2_feature_classifier q2-feature-classifier +q2_feature_table q2-feature-table +q2_fragment_insertion q2-fragment-insertion +q2_metadata q2-metadata +q2_phylogeny q2-phylogeny +q2_quality_control q2-quality-control +q2_quality_filter q2-quality-filter +q2_sample_classifier q2-sample-classifier +q2_taxa q2-taxa +q2_types q2-types +q2cli q2cli +q2templates q2templates +qbrz qbrz +qcat qcat +qcelemental python3-qcelemental +qcengine python3-qcengine +qiime2 qiime +qiskit_aer python3-qiskit-aer +qiskit_ibmq_provider python3-qiskit-ibmq-provider +qiskit_terra python3-qiskit-terra +qpack python3-qpack +qpageview python3-qpageview +qrcode python3-qrcode +qrcodegen python3-qrcodegen +qrencode python3-qrencode +qrtools python3-qrtools +qstylizer python3-qstylizer +qt5reactor python3-qt5reactor +qtconsole python3-qtconsole +qtsass python3-qtsass +quantities python3-quantities +quark_sphinx_theme python3-quark-sphinx-theme +quart python3-quart +questplus python3-questplus +queuelib python3-queuelib +quickcal quickcal +quisk quisk +quodlibet exfalso +qutebrowser qutebrowser +qutip python3-qutip +qweborf qweborf +rabbitvcs rabbitvcs-core +raccoon python3-raccoon +radio_beam python3-radio-beam +radon radon +rados python3-rados +ragout ragout +railroad_diagrams python3-railroad-diagrams +rally python3-rally +rally_openstack python3-rally-openstack +random2 python3-random2 +randomize python3-randomize +rangehttpserver python3-rangehttpserver +ranger_fm ranger +rapid_photo_downloader rapid-photo-downloader +rarfile python3-rarfile +raritan_json_rpc python3-raritan-json-rpc +rasterio python3-rasterio +ratelimiter python3-ratelimiter +razercfg razercfg +rbd python3-rbd +rcon python3-rcon +rcssmin python3-rcssmin +rcutils python3-rcutils +rdflib python3-rdflib +rdflib_jsonld python3-rdflib-jsonld +rdiff_backup rdiff-backup +readability_lxml python3-readability +readlike python3-readlike +readme_renderer python3-readme-renderer +readucks readucks +rebound_cli rebound +rebulk python3-rebulk +recan recan +reclass python3-reclass +recollchm recollcmd +recommonmark python3-recommonmark +recurring_ical_events python3-recurring-ical-events +redbaron python3-redbaron +redfishtool redfishtool +redis python3-redis +redis_py_cluster python3-rediscluster +redisearch python3-redisearch-py +rednose python3-rednose +reentry python3-reentry +reflink python3-reflink +refstack_client refstack-client +refurb python3-refurb +regex python3-regex +regions python3-regions +relational python3-relational +relational_gui relational +relational_readline relational-cli +relatorio python3-relatorio +releases python3-releases +remote_logon_config_agent remote-logon-config-agent +rename_flac rename-flac +rencode python3-rencode +reno python3-reno +reportbug python3-reportbug +reportlab python3-reportlab +repoze.lru python3-repoze.lru +repoze.sphinx.autointerface python3-repoze.sphinx.autointerface +repoze.tm2 python3-repoze.tm2 +repoze.who python3-repoze.who +reproject python3-reproject +reprotest reprotest +reprounzip python3-reprounzip +reprozip python3-reprozip +requests python3-requests +requests_aws python3-awsauth +requests_cache python3-requests-cache +requests_file python3-requests-file +requests_futures python3-requests-futures +requests_kerberos python3-requests-kerberos +requests_mock python3-requests-mock +requests_ntlm python3-requests-ntlm +requests_oauthlib python3-requests-oauthlib +requests_toolbelt python3-requests-toolbelt +requests_unixsocket python3-requests-unixsocket +requestsexceptions python3-requestsexceptions +requirements_detector python3-requirements-detector +requirements_parser python3-requirement-parser +resampy python3-resampy +resolvelib python3-resolvelib +resource_retriever python3-resource-retriever +responses python3-responses +restless python3-restless +restructuredtext_lint python3-restructuredtext-lint +retry python3-retry +retrying python3-retrying +retweet retweet +reuse reuse +rfc3161ng python3-rfc3161ng +rfc3986 python3-rfc3986 +rfc3987 python3-rfc3987 +rfc6555 python3-rfc6555 +rgw python3-rgw +rich python3-rich +rich_click python3-rich-click +rickslab_gpu_utils python3-gpumodules +rioxarray python3-rioxarray +ripe.atlas.cousteau python3-ripe-atlas-cousteau +ripe.atlas.sagan python3-ripe-atlas-sagan +ripe.atlas.tools ripe-atlas-tools +rjsmin python3-rjsmin +rlp python3-rlp +rnc2rng python3-rnc2rng +rnp python3-rnp +robot_detection python3-robot-detection +rocketcea rocketcea +rocksdb python3-rocksdb +roman python3-roman +rope python3-rope +rosbag python3-rosbag +rosboost_cfg python3-rosboost-cfg +rosclean python3-rosclean +roscreate python3-roscreate +rosdep python3-rosdep2 +rosdiagnostic rosdiagnostic +rosdistro python3-rosdistro +rosgraph python3-rosgraph +rosidl_adapter python3-rosidl +rosidl_cli python3-rosidl +rosidl_cmake python3-rosidl +rosidl_generator_c python3-rosidl +rosidl_generator_cpp python3-rosidl +rosidl_parser python3-rosidl +rosidl_pycommon python3-rosidl +rosidl_typesupport_introspection_c python3-rosidl +rosidl_typesupport_introspection_cpp python3-rosidl +rosinstall python3-rosinstall +rosinstall_generator python3-rosinstall-generator +roslaunch python3-roslaunch +roslib python3-roslib +roslz4 python3-roslz4 +rosmake python3-rosmake +rosmaster python3-rosmaster +rosmsg python3-rosmsg +rosnode python3-rosnode +rosparam python3-rosparam +rospkg python3-rospkg +rospy python3-rospy +rosservice python3-rosservice +rostest python3-rostest +rostopic python3-rostopic +rosunit python3-rosunit +roswtf python3-roswtf +roundrobin python3-roundrobin +rows python3-rows +rpaths python3-rpaths +rpl rpl +rply python3-rply +rpm python3-rpm +rpmlint rpmlint +rpy2 python3-rpy2 +rpyc python3-rpyc +rq python3-rq +rrdtool python3-rrdtool +rsa python3-rsa +rss2email rss2email +rst2pdf rst2pdf +rstcheck python3-rstcheck +rstcheck_core python3-rstcheck +rstr python3-rstr +rt python3-rt +rtslib_fb python3-rtslib-fb +rtv rtv +ruamel.yaml python3-ruamel.yaml +ruamel.yaml.clib python3-ruamel.yaml.clib +rubber rubber +ruffus python3-ruffus +rules python3-django-rules +rviz python3-rviz +s3cmd s3cmd +s3transfer python3-s3transfer +s_tui s-tui +sabyenc3 python3-sabyenc +sadisplay python3-sadisplay +safeeyes safeeyes +sagemath_standard python3-sage +sagenb_export python3-sagenb-export +sahara python3-sahara +sahara_dashboard python3-sahara-dashboard +sahara_plugin_spark python3-sahara-plugin-spark +sahara_plugin_vanilla python3-sahara-plugin-vanilla +salmid salmid +salt salt-common +salt_pepper salt-pepper +saneyaml python3-saneyaml +sanlock_python python3-sanlock +sardana python3-sardana +sarif_om python3-sarif-python-om +sarsen python3-sarsen +sasdata python3-sasdata +sasmodels python3-sasmodels +sasview python3-sasview +satellite_tpikonen satellite-gtk +satpy python3-satpy +sbws sbws +scalene python3-scalene +scantree python3-scantree +scapy python3-scapy +schedule python3-schedule +schedutils python3-schedutils +schema python3-schema +schema_salad python3-schema-salad +schroot python3-schroot +scikit_bio python3-skbio +scikit_build python3-skbuild +scikit_fmm python3-scikit-fmm +scikit_image python3-skimage +scikit_learn python3-sklearn +scikit_misc python3-skmisc +scikit_rf python3-scikit-rf +scitrack python3-scitrack +scoary scoary +scour python3-scour +scp python3-scp +scrape_schema_recipe python3-scrape-schema-recipe +scrapy_djangoitem python3-scrapy-djangoitem +screed python3-screed +screeninfo python3-screeninfo +screenkey screenkey +scripttest python3-scripttest +scriv scriv +scruffington python3-scruffy +scrypt python3-scrypt +sdaps sdaps +sdkmanager sdkmanager +sdnotify python3-sdnotify +seaborn python3-seaborn +searx python3-searx +securesystemslib python3-securesystemslib +sedparse python3-sedparse +sedsed sedsed +segno python3-segno +segyio python3-segyio +seirsplus python3-seirsplus +selenium python3-selenium +selinux python3-selinux +semantic_version python3-semantic-version +semver python3-semver +sen sen +senlin python3-senlin +senlin_dashboard python3-senlin-dashboard +senlin_tempest_plugin senlin-tempest-plugin +sensor_msgs python3-sensor-msgs +sentencepiece python3-sentencepiece +sentinels python3-sentinels +sentinelsat python3-sentinelsat +sentry_sdk python3-sentry-sdk +sep python3-sep +sepolicy python3-sepolicy +sepp sepp +seqdiag python3-seqdiag +seqmagick seqmagick +serializable python3-serializable +serpent python3-serpent +serverfiles python3-serverfiles +service_identity python3-service-identity +session_info python3-sinfo +setools python3-setools +setoptconf python3-setoptconf +setproctitle python3-setproctitle +setuptools python3-pkg-resources +setuptools_gettext python3-setuptools-gettext +setuptools_git python3-setuptools-git +setuptools_protobuf python3-setuptools-protobuf +setuptools_rust python3-setuptools-rust +setuptools_scm python3-setuptools-scm +setuptools_scm_git_archive python3-setuptools-scm-git-archive +sexpdata python3-sexpdata +sfepy python3-sfepy +sgmllib3k python3-sgmllib3k +sgp4 python3-sgp4 +sh python3-sh +shade python3-shade +shellescape python3-shellescape +shellingham python3-shellingham +shelxfile python3-shelxfile +shodan python3-shodan +shortuuid python3-shortuuid +show_in_file_manager python3-showinfilemanager +shtab python3-shtab +siconos python3-siconos +sievelib python3-sievelib +signedjson python3-signedjson +silkaj silkaj +silver_platter silver-platter +silx python3-silx +simple_ccsm simple-ccsm +simple_cdd python3-simple-cdd +simplebayes python3-simplebayes +simpleeval python3-simpleeval +simplegeneric python3-simplegeneric +simplejson python3-simplejson +simplematch python3-simplematch +simplenote python3-simplenote +simpleobsws python3-simpleobsws +simpy python3-simpy3 +sip python3-sipbuild +siphashc python3-siphashc +sireader python3-sireader +siridb_connector python3-siridb-connector +six python3-six +sixer sixer +sklearn_pandas python3-sklearn-pandas +skorch python3-skorch +skyfield python3-skyfield +skytools python3-skytools +slimit python3-slimit +slimmer python3-slimmer +slixmpp python3-slixmpp +smart_open python3-smart-open +smartleia python3-smartleia +smartypants python3-smartypants +smbus2 python3-smbus2 +smclib python3-smclib +smmap python3-smmap +smoke_zephyr python3-smoke-zephyr +smstrade python3-smstrade +snakemake snakemake +snappergui snapper-gui +sncosmo python3-sncosmo +sniffio python3-sniffio +sniffles sniffles +snimpy python3-snimpy +snmpsim snmpsim +snowballstemmer python3-snowballstemmer +snuggs python3-snuggs +social_auth_app_django python3-social-django +social_auth_core python3-social-auth-core +socketIO_client python3-socketio-client +socketpool python3-socketpool +socksio python3-socksio +solo1 solo1-cli +sop python3-sop +sopel sopel +sorl_thumbnail python3-sorl-thumbnail +sorted_nearest python3-sorted-nearest +sortedcollections python3-sortedcollections +sortedcontainers python3-sortedcontainers +sos sosreport +soundconverter soundconverter +soundfile python3-soundfile +soundgrain soundgrain +soupsieve python3-soupsieve +spaghetti python3-spaghetti +spake2 python3-spake2 +sparkpost python3-sparkpost +sparse python3-sparse +speaklater python3-speaklater +specan ubertooth +specreduce python3-specreduce +specreduce_data python3-specreduce-data +spectra python3-spectra +spectral python3-spectral +spectral_cube python3-spectral-cube +specutils python3-specutils +speedtest_cli speedtest-cli +speg python3-speg +spf_engine python3-spf-engine +spglib python3-spglib +sphere python3-sphere +sphinx python3-sphinx +sphinx_a4doc python3-sphinx-a4doc +sphinx_argparse python3-sphinx-argparse +sphinx_astropy python3-sphinx-astropy +sphinx_autoapi python3-sphinx-autoapi +sphinx_autobuild python3-sphinx-autobuild +sphinx_autodoc_typehints python3-sphinx-autodoc-typehints +sphinx_automodapi python3-sphinx-automodapi +sphinx_autorun python3-sphinx-autorun +sphinx_basic_ng sphinx-basic-ng +sphinx_book_theme python3-sphinx-book-theme +sphinx_bootstrap_theme python3-sphinx-bootstrap-theme +sphinx_celery python3-sphinx-celery +sphinx_click python3-sphinx-click +sphinx_copybutton python3-sphinx-copybutton +sphinx_feature_classification python3-sphinx-feature-classification +sphinx_gallery python3-sphinx-gallery +sphinx_inline_tabs python3-sphinx-inline-tabs +sphinx_intl sphinx-intl +sphinx_issues python3-sphinx-issues +sphinx_markdown_tables python3-sphinx-markdown-tables +sphinx_multiversion python3-sphinx-multiversion +sphinx_notfound_page python3-sphinx-notfound-page +sphinx_panels python3-sphinx-panels +sphinx_paramlinks python3-sphinx-paramlinks +sphinx_press_theme python3-sphinx-press-theme +sphinx_prompt python3-sphinx-prompt +sphinx_qt_documentation python3-sphinx-qt-documentation +sphinx_remove_toctrees python3-sphinx-remove-toctrees +sphinx_reredirects python3-sphinx-reredirects +sphinx_rst_builder python3-sphinx-rst-builder +sphinx_rtd_theme python3-sphinx-rtd-theme +sphinx_sitemap python3-sphinx-sitemap +sphinx_tabs python3-sphinx-tabs +sphinx_testing python3-sphinx-testing +sphinxcontrib_actdiag python3-sphinxcontrib.actdiag +sphinxcontrib_apidoc python3-sphinxcontrib.apidoc +sphinxcontrib_applehelp python3-sphinxcontrib.applehelp +sphinxcontrib_asyncio python3-sphinxcontrib-asyncio +sphinxcontrib_autoprogram python3-sphinxcontrib.autoprogram +sphinxcontrib_bibtex python3-sphinxcontrib.bibtex +sphinxcontrib_blockdiag python3-sphinxcontrib.blockdiag +sphinxcontrib_devhelp python3-sphinxcontrib.devhelp +sphinxcontrib_ditaa python3-sphinxcontrib.ditaa +sphinxcontrib_doxylink python3-sphinxcontrib.doxylink +sphinxcontrib_htmlhelp python3-sphinxcontrib.htmlhelp +sphinxcontrib_httpdomain python3-sphinxcontrib.httpdomain +sphinxcontrib_jsmath python3-sphinxcontrib.jsmath +sphinxcontrib_log_cabinet python3-sphinxcontrib-log-cabinet +sphinxcontrib_mermaid python3-sphinxcontrib-mermaid +sphinxcontrib_nwdiag python3-sphinxcontrib.nwdiag +sphinxcontrib_pecanwsme python3-sphinxcontrib-pecanwsme +sphinxcontrib_plantuml python3-sphinxcontrib.plantuml +sphinxcontrib_programoutput python3-sphinxcontrib.programoutput +sphinxcontrib_qthelp python3-sphinxcontrib.qthelp +sphinxcontrib_restbuilder python3-sphinxcontrib.restbuilder +sphinxcontrib_seqdiag python3-sphinxcontrib.seqdiag +sphinxcontrib_serializinghtml python3-sphinxcontrib.serializinghtml +sphinxcontrib_spelling python3-sphinxcontrib.spelling +sphinxcontrib_svg2pdfconverter python3-sphinxcontrib.svg2pdfconverter +sphinxcontrib_trio python3-sphinxcontrib.trio +sphinxcontrib_websupport python3-sphinxcontrib.websupport +sphinxcontrib_youtube python3-sphinxcontrib.youtube +sphinxext_opengraph python3-sphinxext-opengraph +sphinxtesters python3-sphinxtesters +spidev python3-spidev +spinners python3-spinners +spoon python3-spoon +spur python3-spur +spyder python3-spyder +spyder_kernels python3-spyder-kernels +spyder_line_profiler python3-spyder-line-profiler +spyder_memory_profiler python3-spyder-memory-profiler +spyder_reports python3-spyder-reports +spyder_unittest python3-spyder-unittest +spyne python3-spyne +sqlacodegen sqlacodegen +sqlalchemy_migrate python3-migrate +sqlglot python3-sqlglot +sqlite_fts4 python3-sqlite-fts4 +sqlite_utils sqlite-utils +sqlitedict python3-sqlitedict +sqlmodel python3-sqlmodel +sqlparse python3-sqlparse +sqlreduce sqlreduce +sqt python3-sqt +srp python3-srp +srsly python3-srsly +srt python3-srt +ssdeep python3-ssdeep +ssh_audit ssh-audit +ssh_import_id ssh-import-id +sshoot sshoot +sshpubkeys python3-sshpubkeys +sshtunnel python3-sshtunnel +sshuttle sshuttle +stack_data python3-stack-data +stardicter python3-stardicter +starlette python3-starlette +static3 python3-static3 +staticsite staticsite +statmake python3-statmake +statsd python3-statsd +statsmodels python3-statsmodels +stdeb python3-stdeb +stdlib_list python3-stdlib-list +stegcracker stegcracker +stem python3-stem +stepic stepic +stestr python3-stestr +stevedore python3-stevedore +stgit stgit +stomp.py python3-stomp +stomper python3-stomper +stone python3-stone +stopit python3-stopit +storm python3-storm +straight.plugin python3-straight.plugin +streamdeck python3-elgato-streamdeck +streamdeck_ui streamdeck-ui +streamlink python3-streamlink +streamz python3-streamz +stressant stressant +strictyaml python3-strictyaml +stringtemplate3 python3-stringtemplate3 +stripe python3-stripe +structlog python3-structlog +stsci.tools python3-stsci.tools +stubserver python3-stubserver +subdownloader subdownloader +sublime_music sublime-music +subliminal python3-subliminal +subprocess_tee python3-subprocess-tee +subuser subuser +subvertpy python3-subvertpy +suds_community python3-suds +suitesparse_graphblas python3-suitesparse-graphblas +sumolib sumo +sunlight python3-sunlight +sunpy python3-sunpy +sunpy_sphinx_theme python3-sunpy-sphinx-theme +suntime python3-suntime +superqt python3-superqt +supervisor supervisor +sure python3-sure +suricata_update suricata-update +surpyvor surpyvor +sushy python3-sushy +sushy_cli python3-sushy-cli +svg.path python3-svg.path +svgelements python3-svgelements +svglib python3-svglib +svgwrite python3-svgwrite +svim svim +swagger_spec_validator python3-swagger-spec-validator +swapper python3-django-swapper +swift python3-swift +swift_bench swift-bench +swiftsc python3-swiftsc +swiglpk python3-swiglpk +sword python3-sword +swugenerator swugenerator +sybil python3-sybil +syllabipy python3-syllabipy +symfit python3-symfit +sympy python3-sympy +synadm synadm +syncplay syncplay-common +syncthing_gtk syncthing-gtk +synphot python3-synphot +systemd_python python3-systemd +systemfixtures python3-systemfixtures +sysv_ipc python3-sysv-ipc +tables python3-tables +tablib python3-tablib +tabulate python3-tabulate +tagpy python3-tagpy +tap.py python3-tap +tap_as_a_service python3-neutron-taas +taskflow python3-taskflow +tasklib python3-tasklib +taskw python3-taskw +taurus python3-taurus +taurus_pyqtgraph python3-taurus-pyqtgraph +tblib python3-tblib +tcolorpy python3-tcolorpy +td_watson python3-watson +telegram_send telegram-send +telemetry_tempest_plugin telemetry-tempest-plugin +tempest python3-tempest +tempest_horizon horizon-tempest-plugin +tempora python3-tempora +tenacity python3-tenacity +termbox python3-termbox +termcolor python3-termcolor +terminado python3-terminado +terminaltables python3-terminaltables +terminator terminator +termineter termineter +tesserocr python3-tesserocr +test_server python3-test-server +testfixtures python3-testfixtures +testing.common.database python3-testing.common.database +testing.mysqld python3-testing.mysqld +testing.postgresql python3-testing.postgresql +testpath python3-testpath +testrepository python3-testrepository +testresources python3-testresources +testscenarios python3-testscenarios +testtools python3-testtools +texext python3-texext +text_unidecode python3-text-unidecode +textdistance python3-textdistance +textfsm python3-textfsm +textile python3-textile +texttable python3-texttable +textual python3-textual +tf python3-tf +tf2_geometry_msgs python3-tf2-geometry-msgs +tf2_kdl python3-tf2-kdl +tf2_py python3-tf2 +tf2_ros python3-tf2-ros +tf2_sensor_msgs python3-tf2-sensor-msgs +tf_conversions python3-tf-conversions +thefuzz python3-thefuzz +thinc python3-thinc +thonny thonny +threadpoolctl python3-threadpoolctl +three_merge python3-three-merge +thrift python3-thrift +thriftpy python3-thriftpy +throttler python3-throttler +tiddit tiddit +tifffile python3-tifffile +tiledb python3-tiledb +time_decode time-decode +timeline python3-timeline +timeout_decorator python3-timeout-decorator +tinyalign python3-tinyalign +tinyarray python3-tinyarray +tinycss python3-tinycss +tinycss2 python3-tinycss2 +tinydb python3-tinydb +tinyobjloader python3-tinyobjloader +tinyrpc python3-tinyrpc +tipp tipp +tkSnack python3-tksnack +tkcalendar tkcalendar +tkrzw python3-tkrzw +tld python3-tld +tldextract python3-tldextract +tldp python3-tldp +tldr.py tldr-py +tlsh python3-tlsh +tmdbsimple python3-tmdbsimple +tmuxp python3-tmuxp +tnetstring3 python3-tnetstring +tnseq_transit tnseq-transit +todoman todoman +toil toil +tokenize_rt python3-tokenize-rt +tomahawk python3-tomahawk +toml python3-toml +tomli python3-tomli +tomli_w python3-tomli-w +tomlkit python3-tomlkit +tomogui python3-tomogui +toolz python3-toolz +toot toot +tooz python3-tooz +topic_tools python3-topic-tools +toposort python3-toposort +topplot python3-topplot +torch python3-torch +torchaudio python3-torchaudio +torchtext python3-torchtext +torchvision python3-torchvision +tornado python3-tornado +toro python3-toro +torrequest python3-torrequest +tortoisehg tortoisehg +tosca_parser python3-tosca-parser +totalopenstation totalopenstation +towncrier towncrier +tox tox +tpm2_pkcs11_tools python3-tpm2-pkcs11-tools +tpm2_pytss python3-tpm2-pytss +tqdm python3-tqdm +traci sumo +trafficserver_exporter prometheus-trafficserver-exporter +traitlets python3-traitlets +traits python3-traits +traitsui python3-traitsui +traittypes python3-traittypes +transaction python3-transaction +transforms3d python3-transforms3d +transip python3-transip +transitions python3-transitions +translate_toolkit python3-translate +translation_finder python3-translation-finder +translationstring python3-translationstring +translitcodec python3-translitcodec +transliterate python3-transliterate +transmissionrpc python3-transmissionrpc +trash_cli trash-cli +traxtor tractor +treq python3-treq +trezor python3-trezor +trimage trimage +trio python3-trio +trio_websocket python3-trio-websocket +trollimage python3-trollimage +trollsift python3-trollsift +trove python3-trove +trove_classifiers python3-trove-classifiers +trove_dashboard python3-trove-dashboard +trove_tempest_plugin trove-tempest-plugin +trufont python3-trufont +trustme python3-trustme +trydiffoscope trydiffoscope +tryton tryton-client +trytond tryton-server +trytond_account tryton-modules-account +trytond_account_asset tryton-modules-account-asset +trytond_account_be tryton-modules-account-be +trytond_account_cash_rounding tryton-modules-account-cash-rounding +trytond_account_credit_limit tryton-modules-account-credit-limit +trytond_account_de_skr03 tryton-modules-account-de-skr03 +trytond_account_deposit tryton-modules-account-deposit +trytond_account_dunning tryton-modules-account-dunning +trytond_account_dunning_email tryton-modules-account-dunning-email +trytond_account_dunning_fee tryton-modules-account-dunning-fee +trytond_account_dunning_letter tryton-modules-account-dunning-letter +trytond_account_es tryton-modules-account-es +trytond_account_eu tryton-modules-account-eu +trytond_account_fr tryton-modules-account-fr +trytond_account_fr_chorus tryton-modules-account-fr-chorus +trytond_account_invoice tryton-modules-account-invoice +trytond_account_invoice_correction tryton-modules-account-invoice-correction +trytond_account_invoice_defer tryton-modules-account-invoice-defer +trytond_account_invoice_history tryton-modules-account-invoice-history +trytond_account_invoice_line_standalone tryton-modules-account-invoice-line-standalone +trytond_account_invoice_secondary_unit tryton-modules-account-invoice-secondary-unit +trytond_account_invoice_stock tryton-modules-account-invoice-stock +trytond_account_payment tryton-modules-account-payment +trytond_account_payment_braintree tryton-modules-account-payment-braintree +trytond_account_payment_clearing tryton-modules-account-payment-clearing +trytond_account_payment_sepa tryton-modules-account-payment-sepa +trytond_account_payment_sepa_cfonb tryton-modules-account-payment-sepa-cfonb +trytond_account_payment_stripe tryton-modules-account-payment-stripe +trytond_account_product tryton-modules-account-product +trytond_account_statement tryton-modules-account-statement +trytond_account_statement_aeb43 tryton-modules-account-statement-aeb43 +trytond_account_statement_coda tryton-modules-account-statement-coda +trytond_account_statement_ofx tryton-modules-account-statement-ofx +trytond_account_statement_rule tryton-modules-account-statement-rule +trytond_account_stock_anglo_saxon tryton-modules-account-stock-anglo-saxon +trytond_account_stock_continental tryton-modules-account-stock-continental +trytond_account_stock_landed_cost tryton-modules-account-stock-landed-cost +trytond_account_stock_landed_cost_weight tryton-modules-account-stock-landed-cost-weight +trytond_account_tax_cash tryton-modules-account-tax-cash +trytond_account_tax_rule_country tryton-modules-account-tax-rule-country +trytond_analytic_account tryton-modules-analytic-account +trytond_analytic_invoice tryton-modules-analytic-invoice +trytond_analytic_purchase tryton-modules-analytic-purchase +trytond_analytic_sale tryton-modules-analytic-sale +trytond_attendance tryton-modules-attendance +trytond_authentication_sms tryton-modules-authentication-sms +trytond_bank tryton-modules-bank +trytond_carrier tryton-modules-carrier +trytond_carrier_percentage tryton-modules-carrier-percentage +trytond_carrier_subdivision tryton-modules-carrier-subdivision +trytond_carrier_weight tryton-modules-carrier-weight +trytond_commission tryton-modules-commission +trytond_commission_waiting tryton-modules-commission-waiting +trytond_company tryton-modules-company +trytond_company_work_time tryton-modules-company-work-time +trytond_country tryton-modules-country +trytond_currency tryton-modules-currency +trytond_customs tryton-modules-customs +trytond_dashboard tryton-modules-dashboard +trytond_edocument_uncefact tryton-modules-edocument-uncefact +trytond_edocument_unece tryton-modules-edocument-unece +trytond_google_maps tryton-modules-google-maps +trytond_incoterm tryton-modules-incoterm +trytond_ldap_authentication tryton-modules-ldap-authentication +trytond_marketing tryton-modules-marketing +trytond_marketing_automation tryton-modules-marketing-automation +trytond_marketing_email tryton-modules-marketing-email +trytond_notification_email tryton-modules-notification-email +trytond_party tryton-modules-party +trytond_party_avatar tryton-modules-party-avatar +trytond_party_relationship tryton-modules-party-relationship +trytond_party_siret tryton-modules-party-siret +trytond_product tryton-modules-product +trytond_product_attribute tryton-modules-product-attribute +trytond_product_classification tryton-modules-product-classification +trytond_product_classification_taxonomic tryton-modules-product-classification-taxonomic +trytond_product_cost_fifo tryton-modules-product-cost-fifo +trytond_product_cost_history tryton-modules-product-cost-history +trytond_product_cost_warehouse tryton-modules-product-cost-warehouse +trytond_product_kit tryton-modules-product-kit +trytond_product_measurements tryton-modules-product-measurements +trytond_product_price_list tryton-modules-product-price-list +trytond_product_price_list_dates tryton-modules-product-price-list-dates +trytond_product_price_list_parent tryton-modules-product-price-list-parent +trytond_production tryton-modules-production +trytond_production_outsourcing tryton-modules-production-outsourcing +trytond_production_routing tryton-modules-production-routing +trytond_production_split tryton-modules-production-split +trytond_production_work tryton-modules-production-work +trytond_production_work_timesheet tryton-modules-production-work-timesheet +trytond_project tryton-modules-project +trytond_project_invoice tryton-modules-project-invoice +trytond_project_plan tryton-modules-project-plan +trytond_project_revenue tryton-modules-project-revenue +trytond_purchase tryton-modules-purchase +trytond_purchase_amendment tryton-modules-purchase-amendment +trytond_purchase_history tryton-modules-purchase-history +trytond_purchase_invoice_line_standalone tryton-modules-purchase-invoice-line-standalone +trytond_purchase_price_list tryton-modules-purchase-price-list +trytond_purchase_request tryton-modules-purchase-request +trytond_purchase_request_quotation tryton-modules-purchase-request-quotation +trytond_purchase_requisition tryton-modules-purchase-requisition +trytond_purchase_secondary_unit tryton-modules-purchase-secondary-unit +trytond_purchase_shipment_cost tryton-modules-purchase-shipment-cost +trytond_sale tryton-modules-sale +trytond_sale_advance_payment tryton-modules-sale-advance-payment +trytond_sale_amendment tryton-modules-sale-amendment +trytond_sale_complaint tryton-modules-sale-complaint +trytond_sale_credit_limit tryton-modules-sale-credit-limit +trytond_sale_discount tryton-modules-sale-discount +trytond_sale_extra tryton-modules-sale-extra +trytond_sale_gift_card tryton-modules-sale-gift-card +trytond_sale_history tryton-modules-sale-history +trytond_sale_invoice_grouping tryton-modules-sale-invoice-grouping +trytond_sale_opportunity tryton-modules-sale-opportunity +trytond_sale_payment tryton-modules-sale-payment +trytond_sale_price_list tryton-modules-sale-price-list +trytond_sale_product_customer tryton-modules-sale-product-customer +trytond_sale_promotion tryton-modules-sale-promotion +trytond_sale_promotion_coupon tryton-modules-sale-promotion-coupon +trytond_sale_secondary_unit tryton-modules-sale-secondary-unit +trytond_sale_shipment_cost tryton-modules-sale-shipment-cost +trytond_sale_shipment_grouping tryton-modules-sale-shipment-grouping +trytond_sale_shipment_tolerance tryton-modules-sale-shipment-tolerance +trytond_sale_stock_quantity tryton-modules-sale-stock-quantity +trytond_sale_subscription tryton-modules-sale-subscription +trytond_sale_subscription_asset tryton-modules-sale-subscription-asset +trytond_sale_supply tryton-modules-sale-supply +trytond_sale_supply_drop_shipment tryton-modules-sale-supply-drop-shipment +trytond_sale_supply_production tryton-modules-sale-supply-production +trytond_stock tryton-modules-stock +trytond_stock_assign_manual tryton-modules-stock-assign-manual +trytond_stock_consignment tryton-modules-stock-consignment +trytond_stock_forecast tryton-modules-stock-forecast +trytond_stock_inventory_location tryton-modules-stock-inventory-location +trytond_stock_location_move tryton-modules-stock-location-move +trytond_stock_location_sequence tryton-modules-stock-location-sequence +trytond_stock_lot tryton-modules-stock-lot +trytond_stock_lot_sled tryton-modules-stock-lot-sled +trytond_stock_lot_unit tryton-modules-stock-lot-unit +trytond_stock_package tryton-modules-stock-package +trytond_stock_package_shipping tryton-modules-stock-package-shipping +trytond_stock_package_shipping_dpd tryton-modules-stock-package-shipping-dpd +trytond_stock_package_shipping_ups tryton-modules-stock-package-shipping-ups +trytond_stock_product_location tryton-modules-stock-product-location +trytond_stock_quantity_early_planning tryton-modules-stock-quantity-early-planning +trytond_stock_quantity_issue tryton-modules-stock-quantity-issue +trytond_stock_secondary_unit tryton-modules-stock-secondary-unit +trytond_stock_shipment_cost tryton-modules-stock-shipment-cost +trytond_stock_shipment_measurements tryton-modules-stock-shipment-measurements +trytond_stock_split tryton-modules-stock-split +trytond_stock_supply tryton-modules-stock-supply +trytond_stock_supply_day tryton-modules-stock-supply-day +trytond_stock_supply_forecast tryton-modules-stock-supply-forecast +trytond_stock_supply_production tryton-modules-stock-supply-production +trytond_timesheet tryton-modules-timesheet +trytond_timesheet_cost tryton-modules-timesheet-cost +trytond_user_role tryton-modules-user-role +trytond_web_shop tryton-modules-web-shop +trytond_web_shop_vue_storefront tryton-modules-web-shop-vue-storefront +trytond_web_shop_vue_storefront_stripe tryton-modules-web-shop-vue-storefront-stripe +trytond_web_shortener tryton-modules-web-shortener +trytond_web_user tryton-modules-web-user +ttconv python3-ttconv +ttkthemes python3-ttkthemes +ttystatus python3-ttystatus +tuna tuna +tuspy python3-tuspy +tweepy python3-tweepy +twilio python3-twilio +twine twine +twitterwatch twitterwatch +twms twms +twodict python3-twodict +twython python3-twython +txWS python3-txws +txZMQ python3-txzmq +txacme python3-txacme +txaio python3-txaio +txdbus python3-txdbus +txrequests python3-txrequests +txt2tags txt2tags +txtorcon python3-txtorcon +typecatcher typecatcher +typechecks python3-typechecks +typed_ast python3-typed-ast +typedload python3-typedload +typeguard python3-typeguard +typepy python3-typepy +typer python3-typer +types_D3DShot python3-typeshed +types_DateTimeRange python3-typeshed +types_Deprecated python3-typeshed +types_Flask_Cors python3-typeshed +types_Flask_SQLAlchemy python3-typeshed +types_JACK_Client python3-typeshed +types_Markdown python3-typeshed +types_Pillow python3-typeshed +types_PyAutoGUI python3-typeshed +types_PyMySQL python3-typeshed +types_PyScreeze python3-typeshed +types_PyYAML python3-typeshed +types_Pygments python3-typeshed +types_SQLAlchemy python3-typeshed +types_Send2Trash python3-typeshed +types_aiofiles python3-typeshed +types_annoy python3-typeshed +types_appdirs python3-typeshed +types_aws_xray_sdk python3-typeshed +types_babel python3-typeshed +types_backports.ssl_match_hostname python3-typeshed +types_beautifulsoup4 python3-typeshed +types_bleach python3-typeshed +types_boto python3-typeshed +types_braintree python3-typeshed +types_cachetools python3-typeshed +types_caldav python3-typeshed +types_certifi python3-typeshed +types_cffi python3-typeshed +types_chardet python3-typeshed +types_chevron python3-typeshed +types_click_spinner python3-typeshed +types_colorama python3-typeshed +types_commonmark python3-typeshed +types_console_menu python3-typeshed +types_contextvars python3-typeshed +types_croniter python3-typeshed +types_cryptography python3-typeshed +types_dateparser python3-typeshed +types_decorator python3-typeshed +types_dj_database_url python3-typeshed +types_docopt python3-typeshed +types_docutils python3-typeshed +types_editdistance python3-typeshed +types_emoji python3-typeshed +types_entrypoints python3-typeshed +types_first python3-typeshed +types_flake8_2020 python3-typeshed +types_flake8_bugbear python3-typeshed +types_flake8_builtins python3-typeshed +types_flake8_docstrings python3-typeshed +types_flake8_plugin_utils python3-typeshed +types_flake8_rst_docstrings python3-typeshed +types_flake8_simplify python3-typeshed +types_flake8_typing_imports python3-typeshed +types_fpdf2 python3-typeshed +types_gdb python3-typeshed +types_google_cloud_ndb python3-typeshed +types_hdbcli python3-typeshed +types_html5lib python3-typeshed +types_httplib2 python3-typeshed +types_humanfriendly python3-typeshed +types_invoke python3-typeshed +types_jmespath python3-typeshed +types_jsonschema python3-typeshed +types_keyboard python3-typeshed +types_ldap3 python3-typeshed +types_mock python3-typeshed +types_mypy_extensions python3-typeshed +types_mysqlclient python3-typeshed +types_oauthlib python3-typeshed +types_openpyxl python3-typeshed +types_opentracing python3-typeshed +types_paho_mqtt python3-typeshed +types_paramiko python3-typeshed +types_parsimonious python3-typeshed +types_passlib python3-typeshed +types_passpy python3-typeshed +types_peewee python3-typeshed +types_pep8_naming python3-typeshed +types_playsound python3-typeshed +types_polib python3-typeshed +types_prettytable python3-typeshed +types_protobuf python3-typeshed +types_psutil python3-typeshed +types_psycopg2 python3-typeshed +types_pyOpenSSL python3-typeshed +types_pyRFC3339 python3-typeshed +types_pyaudio python3-typeshed +types_pycurl python3-typeshed +types_pyfarmhash python3-typeshed +types_pyflakes python3-typeshed +types_pyinstaller python3-typeshed +types_pynput python3-typeshed +types_pysftp python3-typeshed +types_pytest_lazy_fixture python3-typeshed +types_python_crontab python3-typeshed +types_python_dateutil python3-typeshed +types_python_gflags python3-typeshed +types_python_jose python3-typeshed +types_python_nmap python3-typeshed +types_python_slugify python3-typeshed +types_pytz python3-typeshed +types_pyvmomi python3-typeshed +types_pywin32 python3-typeshed +types_redis python3-typeshed +types_regex python3-typeshed +types_requests python3-typeshed +types_retry python3-typeshed +types_setuptools python3-typeshed +types_simplejson python3-typeshed +types_singledispatch python3-typeshed +types_six python3-typeshed +types_slumber python3-typeshed +types_stdlib_list python3-typeshed +types_stripe python3-typeshed +types_tabulate python3-typeshed +types_termcolor python3-typeshed +types_toml python3-typeshed +types_toposort python3-typeshed +types_tqdm python3-typeshed +types_tree_sitter python3-typeshed +types_tree_sitter_languages python3-typeshed +types_ttkthemes python3-typeshed +types_typed_ast python3-typeshed +types_tzlocal python3-typeshed +types_ujson python3-typeshed +types_urllib3 python3-typeshed +types_vobject python3-typeshed +types_waitress python3-typeshed +types_whatthepatch python3-typeshed +types_xmltodict python3-typeshed +types_xxhash python3-typeshed +types_zxcvbn python3-typeshed +typing_extensions python3-typing-extensions +typing_inspect python3-typing-inspect +typogrify python3-typogrify +tz_converter tz-converter +tzlocal python3-tzlocal +uTidylib python3-utidylib +u_msgpack_python python3-u-msgpack +ua_parser python3-ua-parser +uamqp python3-uamqp +ubuntu_dev_tools python3-ubuntutools +uc_micro_py python3-uc-micro +udatetime python3-udatetime +udiskie udiskie +ueberzug ueberzug +uflash python3-uflash +ufo2ft python3-ufo2ft +ufo2otf ufo2otf +ufoLib2 python3-ufolib2 +ufoProcessor python3-ufoprocessor +ufo_extractor python3-ufo-extractor +ufonormalizer python3-ufonormalizer +ufw ufw +uhashring python3-uhashring +ujson python3-ujson +ulmo python3-ulmo +umap_learn umap-learn +umis umis +unattended_upgrades unattended-upgrades +uncalled uncalled +uncertainties python3-uncertainties +undertime undertime +unearth python3-unearth +unicodecsv python3-unicodecsv +unicodedata2 python3-unicodedata2 +unicorn python3-unicorn +unicycler unicycler +unidiff python3-unidiff +unifrac python3-unifrac +unittest2 python3-unittest2 +unittest_xml_reporting python3-xmlrunner +unpaddedbase64 python3-unpaddedbase64 +untangle python3-untangle +untokenize python3-untokenize +unyt python3-unyt +upass upass +upstream_ontologist python3-upstream-ontologist +uritemplate python3-uritemplate +uritools python3-uritools +url_normalize python3-url-normalize +urllib3 python3-urllib3 +urlscan urlscan +urlwatch urlwatch +urwid python3-urwid +urwid_readline python3-urwid-readline +urwid_satext python3-urwid-satext +urwid_utils python3-urwid-utils +urwidtrees python3-urwidtrees +usagestats python3-usagestats +usbrelay_py python3-usbrelay +usbsdmux usbsdmux +user_agents python3-user-agents +userpath python3-userpath +utf8_locale python3-utf8-locale +uvicorn python3-uvicorn +uvloop python3-uvloop +validate python3-configobj +validators python3-validators +validictory python3-validictory +vanguards vanguards +variety variety +vcrpy python3-vcr +vcstool vcstool +vcstools python3-vcstools +vcversioner python3-vcversioner +vdf python3-vdf +vdirsyncer vdirsyncer +vedo python3-vedo +vega_datasets python3-vega-datasets +venusian python3-venusian +versioneer python3-versioneer +versiontools python3-versiontools +veusz python3-veusz +vfit vfit +viagee viagee +vine python3-vine +vinetto vinetto +virtaal virtaal +virtnbdbackup virtnbdbackup +virtualenv python3-virtualenv +virtualenv_clone python3-virtualenv-clone +virtualenvwrapper python3-virtualenvwrapper +virustotal_api python3-virustotal-api +visidata visidata +vispy python3-vispy +vistrails vistrails +vit vit +vitrage python3-vitrage +vitrage_dashboard python3-vitrage-dashboard +vitrage_tempest_plugin vitrage-tempest-plugin +vmdb2 vmdb2 +vobject python3-vobject +volatildap python3-volatildap +volatile python3-volatile +voltron voltron +voluptuous python3-voluptuous +voluptuous_serialize python3-voluptuous-serialize +vorta vorta +vsts_cd_manager python3-vsts-cd-manager +vttLib python3-vttlib +vulndb python3-vulndb +vulture vulture +w3lib python3-w3lib +wadllib python3-wadllib +wafw00f wafw00f +waiting python3-waiting +waitress python3-waitress +wajig wajig +wapiti3 wapiti +warlock python3-warlock +wasabi python3-wasabi +watchdog python3-watchdog +watcher_dashboard python3-watcher-dashboard +watcher_tempest_plugin watcher-tempest-plugin +watchgod python3-watchgod +wcag_contrast_ratio python3-wcag-contrast-ratio +wchartype python3-wchartype +wcmatch python3-wcmatch +wcwidth python3-wcwidth +wdlparse python3-wdlparse +weasyprint weasyprint +web.py python3-webpy +webargs python3-webargs +webassets python3-webassets +webcolors python3-webcolors +webdavclient3 python3-webdavclient +webencodings python3-webencodings +weblogo python3-weblogo +websocket_client python3-websocket +websocket_httpd python3-websocketd +websockets python3-websockets +websockify python3-websockify +websploit websploit +webvtt_py python3-webvtt +wfuzz wfuzz +wget python3-wget +whatmaps whatmaps +whatthepatch python3-whatthepatch +wheel python3-wheel +wheezy.template python3-wheezy.template +whichcraft python3-whichcraft +whipper whipper +whisper python3-whisper +whitenoise python3-whitenoise +whois python3-whois +widgetsnbextension python3-widgetsnbextension +wifite wifite +wikitrans python3-wikitrans +wilderness python3-wilderness +wimsapi python3-wimsapi +wither python3-wither +wlc wlc +wokkel python3-wokkel +wordcloud python3-wordcloud +workalendar python3-workalendar +wrapt python3-wrapt +ws4py python3-ws4py +wsaccel python3-wsaccel +wsgi_intercept python3-wsgi-intercept +wsgicors python3-wsgicors +wsgilog python3-wsgilog +wsproto python3-wsproto +wstool python3-wstool +wtf_peewee python3-wtf-peewee +wurlitzer python3-wurlitzer +wxPython python3-wxgtk4.0 +wxmplot python3-wxmplot +wxutils python3-wxutils +x2go python3-x2go +x2gobroker python3-x2gobroker +x_wr_timezone python3-x-wr-timezone +xandikos xandikos +xapers xapers +xapian_haystack python3-xapian-haystack +xarray python3-xarray +xarray_sentinel python3-xarray-sentinel +xattr python3-xattr +xcffib python3-xcffib +xdg python3-xdg +xdo python3-xdo +xdot xdot +xeus_python_shell python3-xeus-python-shell +xgboost python3-xgboost +xhtml2pdf python3-xhtml2pdf +xkcd python3-xkcd +xkcdpass xkcdpass +xlrd python3-xlrd +xlwt python3-xlwt +xmds2 xmds2 +xmldiff xmldiff +xmlschema python3-xmlschema +xmlsec python3-xmlsec +xmltodict python3-xmltodict +xmodem python3-xmodem +xonsh xonsh +xopen python3-xopen +xphyle python3-xphyle +xpore xpore +xpra xpra +xraydb python3-xraydb +xraylarch python3-xraylarch +xrayutilities python3-xrayutilities +xrootd python3-xrootd +xrt python3-xrt +xsdata python3-xsdata +xtermcolor python3-xtermcolor +xvfbwrapper python3-xvfbwrapper +xxdiff_scripts xxdiff-scripts +xxhash python3-xxhash +xyzservices python3-xyzservices +yamllint yamllint +yamlordereddictloader python3-yamlordereddictloader +yanagiba yanagiba +yanc python3-nose-yanc +yanosim yanosim +yapf python3-yapf +yappi python3-yappi +yaql python3-yaql +yara_python python3-yara +yaramod python3-yaramod +yarl python3-yarl +yarsync yarsync +yaswfp python3-yaswfp +yattag python3-yattag +yenc python3-yenc +yokadi yokadi +yowsup python3-yowsup +yoyo_migrations python3-yoyo +yt python3-yt +yt_dlp yt-dlp +yubikey_manager python3-ykman +zVMCloudConnector python3-zvmcloudconnector +zake python3-zake +zaqar python3-zaqar +zaqar_tempest_plugin zaqar-tempest-plugin +zaqar_ui python3-zaqar-ui +zarr python3-zarr +zc.buildout python3-zc.buildout +zc.customdoctests python3-zc.customdoctests +zc.lockfile python3-zc.lockfile +zeep python3-zeep +zeroconf python3-zeroconf +zfec python3-zfec +zict python3-zict +zigpy python3-zigpy +zim zim +zipp python3-zipp +zipstream python3-zipstream +zipstream_ng python3-zipstream-ng +zkg zkg +zktop zktop +zlmdb python3-zlmdb +zodbpickle python3-zodbpickle +zope.component python3-zope.component +zope.configuration python3-zope.configuration +zope.deprecation python3-zope.deprecation +zope.event python3-zope.event +zope.exceptions python3-zope.exceptions +zope.hookable python3-zope.hookable +zope.i18nmessageid python3-zope.i18nmessageid +zope.interface python3-zope.interface +zope.location python3-zope.location +zope.proxy python3-zope.proxy +zope.schema python3-zope.schema +zope.security python3-zope.security +zope.testing python3-zope.testing +zope.testrunner python3-zope.testrunner +zstandard python3-zstandard +zstd python3-zstd +zxcvbn python3-zxcvbn +zzzeeksphinx python3-zzzeeksphinx diff --git a/pydist/generate_fallback_list.py b/pydist/generate_fallback_list.py new file mode 100755 index 0000000..5f9e480 --- /dev/null +++ b/pydist/generate_fallback_list.py @@ -0,0 +1,156 @@ +#! /usr/bin/python3 +# Copyright © 2010-2015 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 sys +try: + from distro_info import DistroInfo # python3-distro-info package +except ImportError: + DistroInfo = None +from gzip import decompress +from os import chdir, mkdir +from os.path import dirname, exists, isdir, join, split +from urllib.request import urlopen + +if '--ubuntu' in sys.argv and DistroInfo: + SOURCES = [ + 'http://archive.ubuntu.com/ubuntu/dists/%s/Contents-amd64.gz' % + DistroInfo('ubuntu').devel(), + ] +else: + SOURCES = [ + 'http://ftp.debian.org/debian/dists/unstable/main/Contents-all.gz', + 'http://ftp.debian.org/debian/dists/unstable/main/Contents-amd64.gz', + ] + +IGNORED_PKGS = {'python-setuptools', 'python3-setuptools', 'pypy-setuptools'} +OVERRIDES = { + 'cpython2': { + 'python': 'python', + 'setuptools': 'python-pkg-resources', + 'wsgiref': 'python (>= 2.5) | python-wsgiref', + 'argparse': 'python (>= 2.7) | python-argparse', + # not recognized due to .pth file (egg-info is in PIL/ and not in *-packages/) + 'pil': 'python-pil', + 'Pillow': 'python-pil'}, + 'cpython3': { + 'pil': 'python3-pil', + 'Pillow': 'python3-pil', + 'pylint': 'pylint', + 'setuptools': 'python3-pkg-resources', + 'argparse': 'python3 (>= 3.2)'}, + 'pypy': {} +} + +public_egg = re.compile(r''' + /usr/ + ( + (?P<cpython2> + (lib/python2\.[0-9]/((site)|(dist))-packages)| + (share/python-support/[^/]+) + )| + (?P<cpython3> + (lib/python3/dist-packages) + )| + (?P<pypy> + (lib/pypy/dist-packages) + ) + ) + /[^/]*\.(dist|egg)-info +''', re.VERBOSE).match + +skip_sensible_names = True if '--skip-sensible-names' in sys.argv else False + +chdir(dirname(__file__)) +if isdir('../dhpython'): + sys.path.append('..') +else: + sys.path.append('/usr/share/dh-python/dhpython/') +from dhpython.pydist import sensible_pname + +data = '' +if not isdir('cache'): + mkdir('cache') +for source in SOURCES: + cache_fpath = join('cache', split(source)[-1]) + if not exists(cache_fpath): + with urlopen(source) as fp: + source_data = fp.read() + with open(cache_fpath, 'wb') as fp: + fp.write(source_data) + else: + with open(cache_fpath, 'rb') as fp: + source_data = fp.read() + try: + data += str(decompress(source_data), encoding='UTF-8') + except UnicodeDecodeError as e: # Ubuntu + data += str(decompress(source_data), encoding='ISO-8859-15') + +result = { + 'cpython3': {} +} + +# Contents file doesn't contain comment these days +is_header = not data.startswith('bin') +for line in data.splitlines(): + if is_header: + if line.startswith('FILE'): + is_header = False + continue + try: + path, desc = line.rsplit(maxsplit=1) + except ValueError: + # NOTE(jamespage) some lines in Ubuntu are not parseable. + continue + path = '/' + path.rstrip() + section, pkg_name = desc.rsplit('/', 1) + if pkg_name in IGNORED_PKGS: + continue + match = public_egg(path) + if match: + egg_name = [i.split('-', 1)[0] for i in path.split('/') + if i.endswith(('.egg-info', '.dist-info'))][0] + if egg_name.endswith('.egg'): + egg_name = egg_name[:-4] + + impl = next(key for key, value in match.groupdict().items() if value) + + if skip_sensible_names and\ + sensible_pname(impl, egg_name) == pkg_name: + continue + + if impl not in result: + continue + + processed = result[impl] + if egg_name not in processed: + processed[egg_name] = pkg_name + +for impl, details in result.items(): + with open('{}_fallback'.format(impl), 'w') as fp: + overrides = OVERRIDES[impl] + lines = [] + for egg, value in overrides.items(): + lines.append('{} {}\n'.format(egg, value)) + lines.extend( + '{} {}\n'.format(egg, pkg) for egg, pkg in details.items() if egg not in overrides + ) + fp.writelines(sorted(lines)) diff --git a/tests/Makefile b/tests/Makefile new file mode 100644 index 0000000..55f0c9e --- /dev/null +++ b/tests/Makefile @@ -0,0 +1,20 @@ +#!/usr/bin/make -f + +# enable or disable tests here: +#TESTS := test301 test302 test303 test304 test305 test306 testpb01 testpb02 testpb03 testpb04 testpb05 testpb06 testpb07 testa01 testa02 +TESTS := test301 test302 test303 test304 test305 test306 + +all: $(TESTS) + +test%: + make -C t$* run + make -C t$* check + +clean-test%: + make -C t$* clean + +clean: $(TESTS:%=clean-%) + rm -f *\.dsc *\.tar\.gz *\.build *\.changes *\.deb *\.buildinfo + @find . -prune -name '*.egg-info' -exec rm -rf '{}' ';' || true + +.PHONY: clean diff --git a/tests/common.mk b/tests/common.mk new file mode 100644 index 0000000..b4d4f02 --- /dev/null +++ b/tests/common.mk @@ -0,0 +1,19 @@ +#!/usr/bin/make -f + +export DEBPYTHON3_DEFAULT ?= $(shell python3 ../../dhpython/_defaults.py default cpython3) +export DEBPYTHON3_SUPPORTED ?= $(shell python3 ../../dhpython/_defaults.py supported cpython3) +export DEB_HOST_MULTIARCH=my_multiarch-triplet +export DEB_HOST_ARCH ?= $(shell dpkg-architecture -qDEB_HOST_ARCH) +export DH_INTERNAL_OPTIONS= + +all: run check + +run: clean + @echo ============================================================ + @echo ==== TEST: `basename $$PWD` + dpkg-buildpackage -b -us -uc \ + --no-check-builddeps \ + --check-command="../test-package-show-info" + +clean-common: + ./debian/rules clean diff --git a/tests/common.py b/tests/common.py new file mode 100644 index 0000000..7e974fc --- /dev/null +++ b/tests/common.py @@ -0,0 +1,18 @@ +class FakeOptions: + def __init__(self, **kwargs): + opts = { + 'depends': (), + 'depends_section': (), + 'guess_deps': False, + 'no_ext_rename': False, + 'recommends': (), + 'recommends_section': (), + 'requires': (), + 'suggests': (), + 'suggests_section': (), + 'vrange': None, + 'accept_upstream_versions': False, + } + opts.update(kwargs) + for k, v in opts.items(): + setattr(self, k, v) diff --git a/tests/t301/Makefile b/tests/t301/Makefile new file mode 100644 index 0000000..4be460f --- /dev/null +++ b/tests/t301/Makefile @@ -0,0 +1,15 @@ +#!/usr/bin/make -f +include ../common.mk + +check: + grep -qe "Depends: .*python3\(:any\)\? (<< 3.9)" debian/python3-foo/DEBIAN/control + grep -q "Recommends: .*python3-mako" debian/python3-foo/DEBIAN/control + test -f debian/python3-foo/usr/lib/python3/dist-packages/foo/__init__.py + test ! -f debian/python3-foo/usr/lib/python3/dist-packages/foo/spam.py + grep -q 'py3compile -p python3-foo -V 3\.1-3\.9' debian/python3-foo/DEBIAN/postinst + grep -q 'pypy3compile -p python3-foo -V 3\.1-3\.9' debian/python3-foo/DEBIAN/postinst + grep -q 'py3clean -p python3-foo\s*$$' debian/python3-foo/DEBIAN/prerm + +clean: + ./debian/rules clean + rm -rf lib/Foo.egg-info diff --git a/tests/t301/debian/changelog b/tests/t301/debian/changelog new file mode 100644 index 0000000..0f9a168 --- /dev/null +++ b/tests/t301/debian/changelog @@ -0,0 +1,5 @@ +foo (0.1.1) unstable; urgency=low + + * Initial release + + -- Piotr Ożarowski <piotr@debian.org> Sat, 27 Feb 2010 20:42:17 +0100 diff --git a/tests/t301/debian/compat b/tests/t301/debian/compat new file mode 100644 index 0000000..ec63514 --- /dev/null +++ b/tests/t301/debian/compat @@ -0,0 +1 @@ +9 diff --git a/tests/t301/debian/control b/tests/t301/debian/control new file mode 100644 index 0000000..fc18d76 --- /dev/null +++ b/tests/t301/debian/control @@ -0,0 +1,18 @@ +Source: foo +Section: python +Priority: optional +Maintainer: Piotr Ożarowski <piotr@debian.org> +Build-Depends: debhelper (>= 7.0.50~), python3-all +Standards-Version: 3.9.0 +X-Python3-Version: >= 3.1, << 3.9 + +Architecture: all +Package: python3-foo +Depends: ${python3:Depends}, ${misc:Depends} +Recommends: ${python3:Recommends} +Suggests: ${python3:Suggests} +Enhances: ${python3:Enhances} +Breaks: ${python3:Breaks} +Provides: ${python3:Provides} +Description: foo to rule them all + example package #1 diff --git a/tests/t301/debian/copyright b/tests/t301/debian/copyright new file mode 100644 index 0000000..6382944 --- /dev/null +++ b/tests/t301/debian/copyright @@ -0,0 +1,2 @@ +The Debian packaging is © 2010, Piotr Ożarowski <piotr@debian.org> and +is licensed under the MIT License. diff --git a/tests/t301/debian/py3dist-overrides b/tests/t301/debian/py3dist-overrides new file mode 100644 index 0000000..ab3ccb8 --- /dev/null +++ b/tests/t301/debian/py3dist-overrides @@ -0,0 +1,5 @@ +Mako python3-mako (>= 0.2) +SQLAlchemy python3-sqlalchemy (>= 0.6) +Foo python3-foo +Bar python3-bar +Baz
\ No newline at end of file diff --git a/tests/t301/debian/python3-foo.pydist b/tests/t301/debian/python3-foo.pydist new file mode 100644 index 0000000..82849da --- /dev/null +++ b/tests/t301/debian/python3-foo.pydist @@ -0,0 +1 @@ +SQLAlchemy 3.2- python3-sqlalchemy; PEP386 s/^/1:/ diff --git a/tests/t301/debian/rules b/tests/t301/debian/rules new file mode 100755 index 0000000..9c1bdff --- /dev/null +++ b/tests/t301/debian/rules @@ -0,0 +1,25 @@ +#!/usr/bin/make -f +%: + dh $@ --buildsystem=python_distutils + +override_dh_auto_install: + python3 setup.py install --root=debian/python3-foo/ + +override_dh_install: + dh_install + find debian/ -name jquery.js -exec \ + ln -fs /usr/share/javascript/jquery/jquery.js '{}' \; + DH_VERBOSE=1 ../../dh_python3\ + --depends 'SQLAlchemy >= 0.6.1'\ + --recommends Mako\ + --suggests 'Foo >= 0.1'\ + --suggests 'bar >= 1.0' + +clean: + rm -rf build + dh clean + +override_dh_auto_build: + +override_dh_auto_clean: + #python3 setup.py clean diff --git a/tests/t301/debian/source/format b/tests/t301/debian/source/format new file mode 100644 index 0000000..89ae9db --- /dev/null +++ b/tests/t301/debian/source/format @@ -0,0 +1 @@ +3.0 (native) diff --git a/tests/t301/lib/foo/__init__.py b/tests/t301/lib/foo/__init__.py new file mode 100644 index 0000000..9df03f8 --- /dev/null +++ b/tests/t301/lib/foo/__init__.py @@ -0,0 +1 @@ +print("you just imported foo from %s" % __file__) diff --git a/tests/t301/lib/foo/bar/__init__.py b/tests/t301/lib/foo/bar/__init__.py new file mode 100644 index 0000000..669df66 --- /dev/null +++ b/tests/t301/lib/foo/bar/__init__.py @@ -0,0 +1 @@ +print("you just imported foo.bar from %s" % __file__) diff --git a/tests/t301/lib/foo/baz.py b/tests/t301/lib/foo/baz.py new file mode 100644 index 0000000..934dcfe --- /dev/null +++ b/tests/t301/lib/foo/baz.py @@ -0,0 +1 @@ +print("you just imported foo.baz from %s" % __file__) diff --git a/tests/t301/lib/foo/jquery.js b/tests/t301/lib/foo/jquery.js new file mode 120000 index 0000000..b77fd86 --- /dev/null +++ b/tests/t301/lib/foo/jquery.js @@ -0,0 +1 @@ +/usr/share/javascript/jquery/jquery.js
\ No newline at end of file diff --git a/tests/t301/setup.py b/tests/t301/setup.py new file mode 100644 index 0000000..bb7f37d --- /dev/null +++ b/tests/t301/setup.py @@ -0,0 +1,17 @@ +#! /usr/bin/python3 +from distutils.core import setup + +setup(name='Foo', + version='0.2', + description="package with public modules only", + long_description="TODO", + keywords='foo bar baz', + author='Piotr Ożarowski', + author_email='piotr@debian.org', + url='http://www.debian.org/', + license='MIT', + packages=['foo'], + package_dir={'foo': 'lib/foo'}, + package_data={'foo': ['jquery.js']}, + zip_safe=False, +) diff --git a/tests/t302/Makefile b/tests/t302/Makefile new file mode 100644 index 0000000..2fc6b11 --- /dev/null +++ b/tests/t302/Makefile @@ -0,0 +1,18 @@ +#!/usr/bin/make -f +include ../common.mk + +check: + grep -q "py3compile -p python3-foo:$(DEB_HOST_ARCH) /usr/lib/python3-foo"\ + debian/python3-foo/DEBIAN/postinst + grep -q "pypy3compile -p python3-foo:$(DEB_HOST_ARCH) /usr/lib/python3-foo"\ + debian/python3-foo/DEBIAN/postinst + grep -q "py3clean -p python3-foo:$(DEB_HOST_ARCH)" debian/python3-foo/DEBIAN/prerm + [ "`find debian/python3-foo/usr/lib/python3/dist-packages/foo -name 'bar.cpython-*.so'`" != "" ] + test -e debian/python3-foo/usr/lib/python3-foo/empty-private-dir + test ! -e debian/python3-foo/usr/lib/python3/dist-packages/empty-public-dir + # test if moved from include/python3.X/ to include/python3.Xm/ (for Python << 3.8) + #test -f debian/python3-foo/usr/include/python$(DEBPYTHON3_DEFAULT)m/foo.h + +clean: + ./debian/rules clean + rm -rf lib/Foo.egg-info build diff --git a/tests/t302/debian/changelog b/tests/t302/debian/changelog new file mode 100644 index 0000000..d91e7a7 --- /dev/null +++ b/tests/t302/debian/changelog @@ -0,0 +1,5 @@ +foo (0.1.1) unstable; urgency=low + + * Initial release + + -- Piotr Ożarowski <piotr@debian.org> Sun, 19 Dec 2010 19:40:33 +0100 diff --git a/tests/t302/debian/compat b/tests/t302/debian/compat new file mode 100644 index 0000000..ec63514 --- /dev/null +++ b/tests/t302/debian/compat @@ -0,0 +1 @@ +9 diff --git a/tests/t302/debian/control b/tests/t302/debian/control new file mode 100644 index 0000000..01a7656 --- /dev/null +++ b/tests/t302/debian/control @@ -0,0 +1,18 @@ +Source: foo +Section: python +Priority: optional +Maintainer: Piotr Ożarowski <piotr@debian.org> +Build-Depends: debhelper (>= 7.0.50~), python3-all-dev +Standards-Version: 3.9.1 +X-Python3-Version: >= 3.2 + +Package: python3-foo +Architecture: any +Depends: ${python3:Depends}, ${shlibs:Depends}, ${misc:Depends} +Recommends: ${python3:Recommends} +Suggests: ${python3:Suggests} +Enhances: ${python3:Enhances} +Breaks: ${python3:Breaks} +Provides: ${python3:Provides} +Description: package with public and private Python 3 extension + example package #3 - Python extensions diff --git a/tests/t302/debian/copyright b/tests/t302/debian/copyright new file mode 100644 index 0000000..6382944 --- /dev/null +++ b/tests/t302/debian/copyright @@ -0,0 +1,2 @@ +The Debian packaging is © 2010, Piotr Ożarowski <piotr@debian.org> and +is licensed under the MIT License. diff --git a/tests/t302/debian/install b/tests/t302/debian/install new file mode 100644 index 0000000..61f42c3 --- /dev/null +++ b/tests/t302/debian/install @@ -0,0 +1,2 @@ +# private module in architecture dependent dir +lib/foo.py /usr/lib/python3-foo/ diff --git a/tests/t302/debian/rules b/tests/t302/debian/rules new file mode 100755 index 0000000..a85d729 --- /dev/null +++ b/tests/t302/debian/rules @@ -0,0 +1,30 @@ +#!/usr/bin/make -f + +%: + dh $@ --buildsystem=python_distutils + +override_dh_install: + dh_install + # install also as private extension + dh_install debian/python3-foo/usr/local/lib/python3*/dist-packages/foo/bar*.so \ + /usr/lib/python3-foo/ + mkdir -p debian/python3-foo/usr/lib/python3/dist-packages/empty-public-dir + mkdir -p debian/python3-foo/usr/lib/python3-foo/empty-private-dir + DH_VERBOSE=1 ../../dh_python3 + +comma:=, +empty:= +space:= $(empty) $(empty) +PYTHONS=$(subst $(comma),$(space),$(DEBPYTHON3_SUPPORTED)) +override_dh_auto_build: + for ver in $(PYTHONS); do\ + python$$ver setup.py build; done + +override_dh_auto_install: + for ver in $(PYTHONS); do\ + python$$ver setup.py install --root=debian/python3-foo;\ + done + mkdir -p debian/python3-foo/usr/include/python$(DEBPYTHON3_DEFAULT)/ + touch debian/python3-foo/usr/include/python$(DEBPYTHON3_DEFAULT)/foo.h + +override_dh_auto_clean: diff --git a/tests/t302/debian/source/format b/tests/t302/debian/source/format new file mode 100644 index 0000000..89ae9db --- /dev/null +++ b/tests/t302/debian/source/format @@ -0,0 +1 @@ +3.0 (native) diff --git a/tests/t302/lib/__init__.py b/tests/t302/lib/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/t302/lib/__init__.py diff --git a/tests/t302/lib/bar.c b/tests/t302/lib/bar.c new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/t302/lib/bar.c diff --git a/tests/t302/lib/foo.py b/tests/t302/lib/foo.py new file mode 100644 index 0000000..9dd52e6 --- /dev/null +++ b/tests/t302/lib/foo.py @@ -0,0 +1,6 @@ +import foo.bar + + +class Foo(object): + def __init__(self): + foo.bar diff --git a/tests/t302/setup.py b/tests/t302/setup.py new file mode 100755 index 0000000..1208046 --- /dev/null +++ b/tests/t302/setup.py @@ -0,0 +1,13 @@ +#! /usr/bin/python3 +from distutils.core import setup, Extension + +setup(name='Foo', + version='0.1', + description="package with Python extension", + author='Piotr Ożarowski', + author_email='piotr@debian.org', + url='http://www.debian.org/', + ext_modules=[Extension('foo/bar', ['lib/bar.c'])], + #py_modules=['package'], + packages=['foo'], + package_dir={'foo': 'lib'}) diff --git a/tests/t303/Makefile b/tests/t303/Makefile new file mode 100644 index 0000000..9f2c71b --- /dev/null +++ b/tests/t303/Makefile @@ -0,0 +1,12 @@ +#!/usr/bin/make -f +include ../common.mk + +check: + test -f debian/python3-foo/usr/lib/python3/dist-packages/foo.py + test ! -d debian/python3-foo/usr/lib/python3.*/site-packages + grep -q 'py3compile -p python3-foo\s*$$' debian/python3-foo/DEBIAN/postinst + grep -q 'pypy3compile -p python3-foo\s*||\s*true$$' debian/python3-foo/DEBIAN/postinst + grep -q 'py3clean -p python3-foo\s*$$' debian/python3-foo/DEBIAN/prerm + +clean: + ./debian/rules clean diff --git a/tests/t303/debian/changelog b/tests/t303/debian/changelog new file mode 100644 index 0000000..1a796a1 --- /dev/null +++ b/tests/t303/debian/changelog @@ -0,0 +1,5 @@ +foo (0.1.1) unstable; urgency=low + + * Initial release + + -- Piotr Ożarowski <piotr@debian.org> Thu, 06 Jan 2011 17:23:23 +0100 diff --git a/tests/t303/debian/compat b/tests/t303/debian/compat new file mode 100644 index 0000000..ec63514 --- /dev/null +++ b/tests/t303/debian/compat @@ -0,0 +1 @@ +9 diff --git a/tests/t303/debian/control b/tests/t303/debian/control new file mode 100644 index 0000000..beb1372 --- /dev/null +++ b/tests/t303/debian/control @@ -0,0 +1,12 @@ +Source: foo +Section: python +Priority: optional +Maintainer: Piotr Ożarowski <piotr@debian.org> +Build-Depends: debhelper (>= 7.0.50~), python3-all-dev +Standards-Version: 3.9.1 + +Package: python3-foo +Architecture: all +Depends: ${python3:Depends}, ${shlibs:Depends}, ${misc:Depends} +Description: package with public Python 3 modules + example package #4 - fix_locations test diff --git a/tests/t303/debian/copyright b/tests/t303/debian/copyright new file mode 100644 index 0000000..69cea75 --- /dev/null +++ b/tests/t303/debian/copyright @@ -0,0 +1,2 @@ +The Debian packaging is © 2011, Piotr Ożarowski <piotr@debian.org> and +is licensed under the MIT License. diff --git a/tests/t303/debian/rules b/tests/t303/debian/rules new file mode 100755 index 0000000..8f0c8be --- /dev/null +++ b/tests/t303/debian/rules @@ -0,0 +1,23 @@ +#!/usr/bin/make -f + +%: + dh $@ + +override_dh_install: + dh_install + DH_VERBOSE=1 ../../dh_python3 + +override_dh_auto_build: +override_dh_auto_test: + +comma:=, +empty:= +space:= $(empty) $(empty) +PYTHONS=$(subst $(comma),$(space),$(DEBPYTHON3_SUPPORTED)) +override_dh_auto_install: + set -e; for ver in $(PYTHONS); do\ + mkdir -p debian/python3-foo/usr/lib/python$$ver/site-packages/;\ + echo "print('foo')" > debian/python3-foo/usr/lib/python$$ver/site-packages/foo.py;\ + done + +override_dh_auto_clean: diff --git a/tests/t303/debian/source/format b/tests/t303/debian/source/format new file mode 100644 index 0000000..89ae9db --- /dev/null +++ b/tests/t303/debian/source/format @@ -0,0 +1 @@ +3.0 (native) diff --git a/tests/t304/Makefile b/tests/t304/Makefile new file mode 100644 index 0000000..d69cdc9 --- /dev/null +++ b/tests/t304/Makefile @@ -0,0 +1,23 @@ +#!/usr/bin/make -f + +include ../common.mk +clean: clean-common + +check: + # python3.2 hardcoded via `dh_python3 --shebang ...python3.2` + grep -q '#! /usr/bin/python3.2 -OO' debian/foo/usr/share/baz32/baz.py + # python3.4 hardcoded via shebang + grep -q '/usr/share/foo \-V 3.4' debian/foo/DEBIAN/postinst + # /env removed from shebang + grep -q '#! /usr/bin/python3' debian/foo/usr/share/bar/bar.py + # /local removed from shebang + grep -q '#! /usr/bin/python3' debian/foo/usr/share/foo/baz.py + grep -q '#! /usr/bin/python3.4' debian/foo/usr/share/foo/foo.py + # make sure /usr/share/doc/ is ignored + grep -q -v "py3compile -p foo /usr/share/doc"\ + debian/foo/DEBIAN/postinst + # -X made it into the postinst + grep -qF "/usr/share/bar -X 'spam.py'" debian/foo/DEBIAN/postinst + # Check argument parsing order + grep -q '#! /usr/bin/python3-dbg' debian/overrides/usr/share/overrides1/foo.py + grep -q '#! /usr/bin/python3-dbg' debian/overrides/usr/share/overrides2/foo.py diff --git a/tests/t304/bar.py b/tests/t304/bar.py new file mode 100755 index 0000000..edecee7 --- /dev/null +++ b/tests/t304/bar.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python3 +"env in shebang" diff --git a/tests/t304/baz.py b/tests/t304/baz.py new file mode 100755 index 0000000..eff389f --- /dev/null +++ b/tests/t304/baz.py @@ -0,0 +1,2 @@ +#!/usr/local/bin/python3 +"/usr/local in shebang" diff --git a/tests/t304/debian/changelog b/tests/t304/debian/changelog new file mode 100644 index 0000000..c1ed13c --- /dev/null +++ b/tests/t304/debian/changelog @@ -0,0 +1,5 @@ +foo (1.0) unstable; urgency=low + + * Initial release + + -- Piotr Ożarowski <piotr@debian.org> Sun, 10 Jun 2012 14:09:45 +0200 diff --git a/tests/t304/debian/compat b/tests/t304/debian/compat new file mode 100644 index 0000000..ec63514 --- /dev/null +++ b/tests/t304/debian/compat @@ -0,0 +1 @@ +9 diff --git a/tests/t304/debian/control b/tests/t304/debian/control new file mode 100644 index 0000000..4dd38b0 --- /dev/null +++ b/tests/t304/debian/control @@ -0,0 +1,19 @@ +Source: foo +Section: misc +Priority: optional +Maintainer: Piotr Ożarowski <piotr@debian.org> +Build-Depends: debhelper (>= 7.0.50~) +Build-Depends-Indep: python3 +Standards-Version: 3.9.3 + +Package: foo +Architecture: all +Depends: ${python3:Depends}, ${misc:Depends} +Description: example 4 - shebangs + example package #4 - shebang related tests + +Package: overrides +Architecture: all +Depends: ${python3:Depends}, ${misc:Depends} +Description: example 4 - command line parsing + example package #4 - command line argument parsing diff --git a/tests/t304/debian/copyright b/tests/t304/debian/copyright new file mode 100644 index 0000000..bf78fd0 --- /dev/null +++ b/tests/t304/debian/copyright @@ -0,0 +1,2 @@ +The Debian packaging is © 2012, Piotr Ożarowski <piotr@debian.org> and +is licensed under the MIT License. diff --git a/tests/t304/debian/examples b/tests/t304/debian/examples new file mode 100644 index 0000000..fe8826b --- /dev/null +++ b/tests/t304/debian/examples @@ -0,0 +1 @@ +foo.py diff --git a/tests/t304/debian/install b/tests/t304/debian/install new file mode 100644 index 0000000..a106b6e --- /dev/null +++ b/tests/t304/debian/install @@ -0,0 +1,7 @@ +foo.py /usr/share/foo/ +baz.py /usr/share/foo/ +spam.py /usr/share/foo/ +bar.py /usr/share/bar/ +spam.py /usr/share/bar/ +baz.py /usr/share/baz32/ +spam.py /usr/share/baz32/ diff --git a/tests/t304/debian/overrides.install b/tests/t304/debian/overrides.install new file mode 100644 index 0000000..23e1322 --- /dev/null +++ b/tests/t304/debian/overrides.install @@ -0,0 +1,2 @@ +foo.py /usr/share/overrides1/ +foo.py /usr/share/overrides2/ diff --git a/tests/t304/debian/rules b/tests/t304/debian/rules new file mode 100755 index 0000000..5de6e3d --- /dev/null +++ b/tests/t304/debian/rules @@ -0,0 +1,26 @@ +#!/usr/bin/make -f +%: + dh $@ --buildsystem=python_distutils + +override_dh_auto_build: +override_dh_auto_install: + +override_dh_install: + dh_install + DH_VERBOSE=1 ../../dh_python3 -p foo + DH_VERBOSE=1 ../../dh_python3 -p foo /usr/share/bar -X spam.py + DH_VERBOSE=1 ../../dh_python3 -p foo /usr/share/baz32 --shebang '/usr/bin/python3.2 -OO' + + # Argument priority: -O > regular args > DH_OPTIONS + DH_VERBOSE=1 DH_OPTIONS=--shebang=/usr/bin/python3.0 \ + ../../dh_python3 -p overrides \ + --shebang=/usr/bin/python3-dbg \ + /usr/share/overrides1 + DH_VERBOSE=1 ../../dh_python3 -p overrides \ + --shebang=/usr/bin/python3.0 \ + -O=--shebang=/usr/bin/python3-dbg \ + -O=--foo=bar \ + /usr/share/overrides2 + +clean: + dh_clean diff --git a/tests/t304/debian/source/format b/tests/t304/debian/source/format new file mode 100644 index 0000000..89ae9db --- /dev/null +++ b/tests/t304/debian/source/format @@ -0,0 +1 @@ +3.0 (native) diff --git a/tests/t304/foo.py b/tests/t304/foo.py new file mode 100755 index 0000000..fef29a7 --- /dev/null +++ b/tests/t304/foo.py @@ -0,0 +1,2 @@ +#!/usr/local/bin/python3.4 +"/usr/local/bin/python3.4 hardcoded in shebang" diff --git a/tests/t304/setup.py b/tests/t304/setup.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/t304/setup.py diff --git a/tests/t304/spam.py b/tests/t304/spam.py new file mode 100644 index 0000000..c8b0fd9 --- /dev/null +++ b/tests/t304/spam.py @@ -0,0 +1 @@ +print('spam') diff --git a/tests/t305/Makefile b/tests/t305/Makefile new file mode 100644 index 0000000..1ee30fc --- /dev/null +++ b/tests/t305/Makefile @@ -0,0 +1,14 @@ +#!/usr/bin/make -f + +include ../common.mk +clean: clean-common + +check: + grep -qe "Depends: .*python3\(:any\)\?" debian/foo5a/DEBIAN/control + grep -qe "Depends: .*python3\(:any\)\?" debian/foo5b/DEBIAN/control + grep -qe "Depends: .*python3\(:any\)\?" debian/foo5c/DEBIAN/control + grep -qe "Depends: .*python3\(:any\)\?" debian/foo5d/DEBIAN/control + grep -qe "Depends: .*python3\(:any\)\?" debian/foo5e/DEBIAN/control + grep -qe "Depends: .*python3\(:any\)\?" debian/foo5f/DEBIAN/control + grep -Fxq dh_python3 debian/foo5a.debhelper.log.end_dh_install + grep -Fxc dh_python3 debian/foo5a.debhelper.log.end_dh_install | grep -Fxq 1 diff --git a/tests/t305/debian/changelog b/tests/t305/debian/changelog new file mode 100644 index 0000000..d30d06c --- /dev/null +++ b/tests/t305/debian/changelog @@ -0,0 +1,5 @@ +foo (1.0) unstable; urgency=low + + * Initial release + + -- Maximilian Engelhardt <maxi@daemonizer.de> Fri, 02 Apr 2021 14:30:25 +0200 diff --git a/tests/t305/debian/compat b/tests/t305/debian/compat new file mode 100644 index 0000000..48082f7 --- /dev/null +++ b/tests/t305/debian/compat @@ -0,0 +1 @@ +12 diff --git a/tests/t305/debian/control b/tests/t305/debian/control new file mode 100644 index 0000000..8eafc3e --- /dev/null +++ b/tests/t305/debian/control @@ -0,0 +1,43 @@ +Source: foo +Section: misc +Priority: optional +Maintainer: Maximilian Engelhardt <maxi@daemonizer.de> +Build-Depends: debhelper (>= 12.9) +Build-Depends-Indep: python3 +Standards-Version: 4.5.1 + +Package: foo5a +Architecture: all +Depends: ${python3:Depends}, ${misc:Depends} +Description: example 5a - private shebangs + example package #5 - shebang related tests in private dirs + +Package: foo5b +Architecture: all +Depends: ${python3:Depends}, ${misc:Depends} +Description: example 5b - private shebangs + example package #5 - shebang related tests in private dirs + +Package: foo5c +Architecture: all +Depends: ${python3:Depends}, ${misc:Depends} +Description: example 5c - private shebangs + example package #5 - shebang related tests in private dirs + +Package: foo5d +Architecture: all +Depends: ${python3:Depends}, ${misc:Depends} +Description: example 5d - private shebangs + example package #5 - shebang related tests in private dirs + +Package: foo5e +Architecture: all +Depends: ${python3:Depends}, ${misc:Depends} +Description: example 5e - private shebangs + example package #5 - shebang related tests in private dirs + +Package: foo5f +Architecture: all +Depends: ${python3:Depends}, ${misc:Depends} +Description: example 5f - private shebangs + example package #5 - shebang related tests in private dirs diff --git a/tests/t305/debian/copyright b/tests/t305/debian/copyright new file mode 100644 index 0000000..32bbc0d --- /dev/null +++ b/tests/t305/debian/copyright @@ -0,0 +1,2 @@ +The Debian packaging is © 2021, Maximilian Engelhardt <maxi@daemonizer.de> and +is licensed under the MIT License. diff --git a/tests/t305/debian/foo5a.install b/tests/t305/debian/foo5a.install new file mode 100644 index 0000000..38009bc --- /dev/null +++ b/tests/t305/debian/foo5a.install @@ -0,0 +1 @@ +foo5a /usr/share/foo/ diff --git a/tests/t305/debian/foo5b.install b/tests/t305/debian/foo5b.install new file mode 100644 index 0000000..0fd0869 --- /dev/null +++ b/tests/t305/debian/foo5b.install @@ -0,0 +1 @@ +foo5b /usr/share/foo/ diff --git a/tests/t305/debian/foo5c.install b/tests/t305/debian/foo5c.install new file mode 100644 index 0000000..a40b010 --- /dev/null +++ b/tests/t305/debian/foo5c.install @@ -0,0 +1 @@ +foo5c /usr/share/foo/ diff --git a/tests/t305/debian/foo5d.install b/tests/t305/debian/foo5d.install new file mode 100644 index 0000000..e6e2aea --- /dev/null +++ b/tests/t305/debian/foo5d.install @@ -0,0 +1 @@ +foo5d /usr/share/foo/ diff --git a/tests/t305/debian/rules b/tests/t305/debian/rules new file mode 100755 index 0000000..c43eaae --- /dev/null +++ b/tests/t305/debian/rules @@ -0,0 +1,22 @@ +#!/usr/bin/make -f +%: + dh $@ --buildsystem=none + +override_dh_auto_install: + dh_auto_install + mkdir -p debian/foo5e/usr/share/foo/ + echo "#! /usr/bin/env $(shell py3versions -d)\n\"/usr/bin/env DEFAULT_PYTHON shebang\"" > debian/foo5e/usr/share/foo/foo5e + chmod +x debian/foo5e/usr/share/foo/foo5e + mkdir -p debian/foo5f/usr/share/foo/ + echo "#! /usr/bin/$(shell py3versions -d)\n\"/usr/bin/DEFAULT_PYTHON shebang\"" > debian/foo5f/usr/share/foo/foo5f + chmod +x debian/foo5f/usr/share/foo/foo5f + +override_dh_install: + dh_install + DH_VERBOSE=1 ../../dh_python3 -p foo5a /usr/share/foo + DH_VERBOSE=1 ../../dh_python3 --remaining-packages /usr/share/foo + cp debian/foo5a.debhelper.log debian/foo5a.debhelper.log.end_dh_install + +clean: + rm -f debian/foo5a.debhelper.log.end_dh_install + dh_clean diff --git a/tests/t305/debian/source/format b/tests/t305/debian/source/format new file mode 100644 index 0000000..89ae9db --- /dev/null +++ b/tests/t305/debian/source/format @@ -0,0 +1 @@ +3.0 (native) diff --git a/tests/t305/foo5a b/tests/t305/foo5a new file mode 100755 index 0000000..3045ad0 --- /dev/null +++ b/tests/t305/foo5a @@ -0,0 +1,2 @@ +#! /usr/bin/python3 +"/usr/bin/python3 shebang" diff --git a/tests/t305/foo5b b/tests/t305/foo5b new file mode 100755 index 0000000..49352f5 --- /dev/null +++ b/tests/t305/foo5b @@ -0,0 +1,2 @@ +#! /usr/bin/python +"/usr/bin/python shebang" diff --git a/tests/t305/foo5c b/tests/t305/foo5c new file mode 100755 index 0000000..2ee0891 --- /dev/null +++ b/tests/t305/foo5c @@ -0,0 +1,2 @@ +#! /usr/bin/env python3 +"/usr/bin/env python3 shebang" diff --git a/tests/t305/foo5d b/tests/t305/foo5d new file mode 100755 index 0000000..b698372 --- /dev/null +++ b/tests/t305/foo5d @@ -0,0 +1,2 @@ +#! /usr/bin/env python +"/usr/bin/env python shebang" diff --git a/tests/t306/Makefile b/tests/t306/Makefile new file mode 100644 index 0000000..9dbf8c4 --- /dev/null +++ b/tests/t306/Makefile @@ -0,0 +1,17 @@ +#!/usr/bin/make -f +include ../common.mk + +all: run check + +run: clean + dpkg-buildpackage -b -us -uc + +check: + grep -qe "^Depends: .*python3\(:any\)\? (<< 3\.[0-9]\+)" \ + debian/foo/DEBIAN/control + grep -qe "^Depends: .*python3\(:any\)\? (>= 3\.[0-9]\+~)" \ + debian/foo/DEBIAN/control + +clean: + ./debian/rules clean + rm -rf lib/Foo.egg-info build diff --git a/tests/t306/debian/changelog b/tests/t306/debian/changelog new file mode 100644 index 0000000..874dc1d --- /dev/null +++ b/tests/t306/debian/changelog @@ -0,0 +1,5 @@ +foo (0.1) unstable; urgency=low + + * Initial release + + -- Maximilian Engelhardt <maxi@daemonizer.de> Sun, 04 Apr 2021 13:09:48 +0200 diff --git a/tests/t306/debian/compat b/tests/t306/debian/compat new file mode 100644 index 0000000..48082f7 --- /dev/null +++ b/tests/t306/debian/compat @@ -0,0 +1 @@ +12 diff --git a/tests/t306/debian/control b/tests/t306/debian/control new file mode 100644 index 0000000..79446f3 --- /dev/null +++ b/tests/t306/debian/control @@ -0,0 +1,13 @@ +Source: foo +Section: python +Priority: optional +Maintainer: Maximilian Engelhardt <maxi@daemonizer.de> +Build-Depends: debhelper (>= 12.9), python3-all-dev:any +Standards-Version: 4.5.1 + +Package: foo +Architecture: any +Depends: ${python3:Depends}, ${shlibs:Depends}, ${misc:Depends} +Description: package with python3-all-dev dependency using :any + example package #6 - private Python extensions and python3-all-dev:any + dependency diff --git a/tests/t306/debian/copyright b/tests/t306/debian/copyright new file mode 100644 index 0000000..32bbc0d --- /dev/null +++ b/tests/t306/debian/copyright @@ -0,0 +1,2 @@ +The Debian packaging is © 2021, Maximilian Engelhardt <maxi@daemonizer.de> and +is licensed under the MIT License. diff --git a/tests/t306/debian/rules b/tests/t306/debian/rules new file mode 100755 index 0000000..d1ffc6a --- /dev/null +++ b/tests/t306/debian/rules @@ -0,0 +1,14 @@ +#!/usr/bin/make -f + +%: + dh $@ --buildsystem=none + +override_dh_install: + dh_install + DH_VERBOSE=1 ../../dh_python3 + +override_dh_auto_build: + python3 setup.py build + +override_dh_auto_install: + python3 setup.py install --root=debian/foo/ --install-lib=/usr/share/foo/python diff --git a/tests/t306/debian/source/format b/tests/t306/debian/source/format new file mode 100644 index 0000000..89ae9db --- /dev/null +++ b/tests/t306/debian/source/format @@ -0,0 +1 @@ +3.0 (native) diff --git a/tests/t306/lib/__init__.py b/tests/t306/lib/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/t306/lib/__init__.py diff --git a/tests/t306/lib/bar.c b/tests/t306/lib/bar.c new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/t306/lib/bar.c diff --git a/tests/t306/lib/foo.py b/tests/t306/lib/foo.py new file mode 100644 index 0000000..9dd52e6 --- /dev/null +++ b/tests/t306/lib/foo.py @@ -0,0 +1,6 @@ +import foo.bar + + +class Foo(object): + def __init__(self): + foo.bar diff --git a/tests/t306/setup.py b/tests/t306/setup.py new file mode 100755 index 0000000..25727f2 --- /dev/null +++ b/tests/t306/setup.py @@ -0,0 +1,12 @@ +#! /usr/bin/python3 +from distutils.core import setup, Extension + +setup(name='Foo', + version='0.1', + description="package with private Python extension", + author='Maximilian Engelhardt', + author_email='maxi@daemonizer.de', + url='http://www.debian.org/', + ext_modules=[Extension('foo/bar', ['lib/bar.c'])], + packages=['foo'], + package_dir={'foo': 'lib'}) diff --git a/tests/ta01/Makefile b/tests/ta01/Makefile new file mode 100644 index 0000000..65c9796 --- /dev/null +++ b/tests/ta01/Makefile @@ -0,0 +1,39 @@ +all: run check + +run: + @echo No build needed +ifeq ($(AUTOPKGTEST_TMP),) + @echo NOTE this test uses the system pybuild-autopkgtest, not the working directory +endif + +check: pass fail + +pass: + @echo "==============================================================" + @echo "= pybuild-autopkgtest passes when tests pass =" + @echo "==============================================================" + @echo + pybuild-autopkgtest + test -f marker-before-pybuild-autopkgtest + test -f marker-after-pybuild-autopkgtest + grep '^1$$' marker-PYBUILD_AUTOPKGTEST + @echo '------------------------------' + @echo "OK: pybuild-autopkgtest passed" + @echo '------------------------------' + @echo + +fail: + @echo "==============================================================" + @echo "= pybuild-autopkgtest fails when tests fail =" + @echo "==============================================================" + @echo + ! FAILS=1 pybuild-autopkgtest + @echo '------------------------------' + @echo "OK: pybuild-autopkgtest failed" + @echo '------------------------------' + @echo + +clean: +ifneq ($(AUTOPKGTEST_TMP),) + rm -r $(AUTOPKGTEST_TMP)/* +endif diff --git a/tests/ta01/debian/changelog b/tests/ta01/debian/changelog new file mode 100644 index 0000000..322011c --- /dev/null +++ b/tests/ta01/debian/changelog @@ -0,0 +1,5 @@ +foo (1.2.3) unstable; urgency=low + + * Initial release + + -- Piotr Ozarowski <piotr@debian.org> Tue, 02 Jul 2013 11:02:06 +0200 diff --git a/tests/ta01/debian/control b/tests/ta01/debian/control new file mode 100644 index 0000000..f8922fc --- /dev/null +++ b/tests/ta01/debian/control @@ -0,0 +1,15 @@ +Source: foo +Section: python +Priority: optional +Maintainer: Piotr Ożarowski <piotr@debian.org> +Build-Depends: debhelper-compat (= 12) + , pybuild-plugin-pyproject + , python3-all + , python3-setuptools +Standards-Version: 3.9.4 + +Package: python3-foo +Architecture: any +Depends: ${python3:Depends}, ${shlibs:Depends}, ${misc:Depends} +Description: package with public CPython modules + example package #8 diff --git a/tests/ta01/debian/rules b/tests/ta01/debian/rules new file mode 100755 index 0000000..e53f0aa --- /dev/null +++ b/tests/ta01/debian/rules @@ -0,0 +1,33 @@ +#!/usr/bin/make -f + +export PYBUILD_NAME=foo + +%: + dh $@ + +override_dh_auto_build: + ../../pybuild --build --verbose + +override_dh_auto_install: + ../../pybuild --install + +override_dh_auto_test: + ../../pybuild --test + +override_dh_auto_clean: + ../../pybuild --clean --verbose + rm -rf .pybuild foo.egg-info + +override_dh_installinit: + DH_VERBOSE=1 ../../dh_python3 + dh_installinit + +override_dh_python3: + # ignore any system dh_python3 + +before-pybuild-autopkgtest: + echo $(PYBUILD_AUTOPKGTEST) > marker-PYBUILD_AUTOPKGTEST + touch marker-before-pybuild-autopkgtest + +after-pybuild-autopkgtest: + touch marker-after-pybuild-autopkgtest diff --git a/tests/ta01/foo/__init__.py b/tests/ta01/foo/__init__.py new file mode 100644 index 0000000..92d9a9a --- /dev/null +++ b/tests/ta01/foo/__init__.py @@ -0,0 +1 @@ +"Nothing here" diff --git a/tests/ta01/pyproject.toml b/tests/ta01/pyproject.toml new file mode 100644 index 0000000..9787c3b --- /dev/null +++ b/tests/ta01/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/tests/ta01/setup.cfg b/tests/ta01/setup.cfg new file mode 100644 index 0000000..877dcf7 --- /dev/null +++ b/tests/ta01/setup.cfg @@ -0,0 +1,10 @@ +[metadata] +name = foo +version = 0.1 +description = My package description +long_description = My long description +license = Expat + +[options] +zip_safe = False +packages = find: diff --git a/tests/ta01/tests/__init__.py b/tests/ta01/tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/ta01/tests/__init__.py diff --git a/tests/ta01/tests/test_foo.py b/tests/ta01/tests/test_foo.py new file mode 100644 index 0000000..263a9fb --- /dev/null +++ b/tests/ta01/tests/test_foo.py @@ -0,0 +1,7 @@ +import os +import unittest + +class TestPybuildAutopkgtest(unittest.TestCase): + + def test_pass_or_fails(self): + self.assertIsNone(os.environ.get("FAILS")) diff --git a/tests/ta02/Makefile b/tests/ta02/Makefile new file mode 100644 index 0000000..f6c26ed --- /dev/null +++ b/tests/ta02/Makefile @@ -0,0 +1,17 @@ +all: run check + +run: + @echo No build needed +ifeq ($(AUTOPKGTEST_TMP),) + @echo NOTE this test uses the system pybuild-autopkgtest, not the working directory +endif + +check: + DH_VERBOSE=1 pybuild-autopkgtest + test -e custom-test-executed + +clean: + ./debian/rules clean +ifneq ($(AUTOPKGTEST_TMP),) + rm -r $(AUTOPKGTEST_TMP)/* +endif diff --git a/tests/ta02/debian/changelog b/tests/ta02/debian/changelog new file mode 100644 index 0000000..322011c --- /dev/null +++ b/tests/ta02/debian/changelog @@ -0,0 +1,5 @@ +foo (1.2.3) unstable; urgency=low + + * Initial release + + -- Piotr Ozarowski <piotr@debian.org> Tue, 02 Jul 2013 11:02:06 +0200 diff --git a/tests/ta02/debian/control b/tests/ta02/debian/control new file mode 100644 index 0000000..648c259 --- /dev/null +++ b/tests/ta02/debian/control @@ -0,0 +1,16 @@ +Source: foo +Section: python +Priority: optional +Maintainer: Piotr Ożarowski <piotr@debian.org> +Build-Depends: debhelper-compat (= 12) + , pybuild-plugin-pyproject + , python3-all + , python3-pytest + , python3-setuptools +Standards-Version: 3.9.4 + +Package: python3-foo +Architecture: any +Depends: ${python3:Depends}, ${shlibs:Depends}, ${misc:Depends} +Description: package with public CPython modules + example package #8 diff --git a/tests/ta02/debian/rules b/tests/ta02/debian/rules new file mode 100755 index 0000000..b612080 --- /dev/null +++ b/tests/ta02/debian/rules @@ -0,0 +1,28 @@ +#!/usr/bin/make -f + +export PYBUILD_NAME=foo +export PYBUILD_TEST_CUSTOM=1 +export PYBUILD_TEST_ARGS=touch {dir}/custom-test-executed + +%: + dh $@ + +override_dh_auto_build: + ../../pybuild --build --verbose + +override_dh_auto_install: + ../../pybuild --install + +override_dh_auto_test: + ../../pybuild --test + +override_dh_auto_clean: + ../../pybuild --clean --verbose + rm -rf custom-test-executed foo.egg-info + +override_dh_installinit: + DH_VERBOSE=1 ../../dh_python3 + dh_installinit + +override_dh_python3: + # ignore any system dh_python3 diff --git a/tests/ta02/foo/__init__.py b/tests/ta02/foo/__init__.py new file mode 100644 index 0000000..92d9a9a --- /dev/null +++ b/tests/ta02/foo/__init__.py @@ -0,0 +1 @@ +"Nothing here" diff --git a/tests/ta02/pyproject.toml b/tests/ta02/pyproject.toml new file mode 100644 index 0000000..9787c3b --- /dev/null +++ b/tests/ta02/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/tests/ta02/setup.cfg b/tests/ta02/setup.cfg new file mode 100644 index 0000000..877dcf7 --- /dev/null +++ b/tests/ta02/setup.cfg @@ -0,0 +1,10 @@ +[metadata] +name = foo +version = 0.1 +description = My package description +long_description = My long description +license = Expat + +[options] +zip_safe = False +packages = find: diff --git a/tests/ta02/tests/__init__.py b/tests/ta02/tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/ta02/tests/__init__.py diff --git a/tests/ta02/tests/test_foo.py b/tests/ta02/tests/test_foo.py new file mode 100644 index 0000000..2cac2b2 --- /dev/null +++ b/tests/ta02/tests/test_foo.py @@ -0,0 +1,7 @@ +import unittest + +class TestThatWeDontRunTheseTests(unittest.TestCase): + + def test_fail(self): + # We want the custom test runner to run, not this test suite + self.assertTrue(False) diff --git a/tests/test-package-show-info b/tests/test-package-show-info new file mode 100755 index 0000000..bdea2b4 --- /dev/null +++ b/tests/test-package-show-info @@ -0,0 +1,16 @@ +#!/bin/sh + +set -ue + +changes=$1 +testname=$(basename $PWD) +basedir=$(dirname $changes) + +echo "--------------------------------------------------" +echo "Test $testname has created the following packages:" +for deb in $(sed -nr 's/.* ([^ ]*\.deb)$/\1/p' $changes | sort -u); do + echo "--------------------------------------------------" + echo "PACKAGE $deb:" + dpkg-deb --info "$basedir/$deb" +done +echo "--------------------------------------------------" diff --git a/tests/test_debhelper.py b/tests/test_debhelper.py new file mode 100644 index 0000000..285440c --- /dev/null +++ b/tests/test_debhelper.py @@ -0,0 +1,192 @@ +from tempfile import TemporaryDirectory +import unittest +import os + +from dhpython.debhelper import DebHelper, build_options + + +class DebHelperTestCase(unittest.TestCase): + impl = 'cpython3' + control = [] + options = {} + parse_control = True + + def build_options(self): + return build_options(**self.options) + + def setUp(self): + self.tempdir = TemporaryDirectory() + self.addCleanup(self.tempdir.cleanup) + + old_wd = os.getcwd() + os.chdir(self.tempdir.name) + self.addCleanup(os.chdir, old_wd) + + os.mkdir('debian') + with open('debian/control', 'w') as f: + f.write('\n'.join(self.control)) + if self.parse_control: + self.dh = DebHelper(self.build_options(), impl=self.impl) + + +CONTROL = [ + 'Source: foo-src', + 'Build-Depends: python3-all,', + ' python-all,', + ' bar (<< 2) [amd64],', + ' baz (>= 1.0)', + 'X-Python3-Version: >= 3.1, << 3.10', + '', + 'Architecture: all', + 'Package: python3-foo', + 'Depends: ${python3:Depends}', + '', + 'Package: python3-foo-ext', + 'Architecture: any', + 'Depends: ${python3:Depends}, ' + '# COMMENT', + ' ${shlibs:Depends},', + '', + 'Package: python-foo', + 'Architecture: all', + 'Depends: ${python:Depends}', + '', + '', + 'Package: foo', + 'Architecture: all', + 'Depends: ${python3:Depends}', + '', + '', + 'Package: recfoo', + 'Architecture: all', + 'Recommends: ${python3:Depends}', + '', + '', +] + +class TestControlBlockParsing(DebHelperTestCase): + control = CONTROL + + def test_parses_source(self): + self.assertEqual(self.dh.source_name, 'foo-src') + + def test_parses_build_depends(self): + self.assertEqual(self.dh.build_depends, { + 'python3-all': {None: None}, + 'python-all': {None: None}, + 'bar': {'amd64': '<< 2'}, + 'baz': {None: '>= 1.0'}, + }) + + def test_parses_XPV(self): + self.assertEqual(self.dh.python_version, '>= 3.1, << 3.10') + + def test_parses_packages(self): + self.assertEqual(list(self.dh.packages.keys()), + ['python3-foo', 'python3-foo-ext', 'foo', 'recfoo']) + + def test_parses_arch(self): + self.assertEqual(self.dh.packages['python3-foo-ext']['arch'], 'any') + + def test_parses_arch_all(self): + self.assertEqual(self.dh.packages['python3-foo']['arch'], 'all') + + +class TestControlSkipIndep(DebHelperTestCase): + control = CONTROL + options = { + 'arch': True, + } + + def test_skip_indep(self): + self.assertEqual(list(self.dh.packages.keys()), ['python3-foo-ext']) + + +class TestControlSkipArch(DebHelperTestCase): + control = CONTROL + options = { + 'arch': False, + } + + def test_skip_arch(self): + self.assertEqual(list(self.dh.packages.keys()), + ['python3-foo', 'foo', 'recfoo']) + + +class TestControlSinglePkg(DebHelperTestCase): + control = CONTROL + options = { + 'package': ['python3-foo'], + } + + def test_parses_packages(self): + self.assertEqual(list(self.dh.packages.keys()), ['python3-foo']) + + +class TestControlSkipSinglePkg(DebHelperTestCase): + control = CONTROL + options = { + 'no_package': ['python3-foo'], + } + + def test_parses_packages(self): + self.assertEqual(list(self.dh.packages.keys()), + ['python3-foo-ext', 'foo', 'recfoo']) + + +class TestControlBlockParsingPy2(DebHelperTestCase): + control = CONTROL + impl = 'cpython2' + + def test_parses_packages(self): + self.assertEqual(list(self.dh.packages.keys()), ['python-foo']) + + +class TestControlNoBinaryPackages(DebHelperTestCase): + control = [ + 'Source: foo-src', + 'Build-Depends: python3-all', + '', + ] + parse_control = False + + def test_throws_error(self): + msg = ('Unable to parse debian/control, found less than 2 paragraphs') + with self.assertRaisesRegex(Exception, msg): + DebHelper(self.build_options()) + + +class TestControlMissingPackage(DebHelperTestCase): + control = [ + 'Source: foo-src', + 'Build-Depends: python3-all', + '', + 'Architecture: all', + ] + parse_control = False + + def test_parses_packages(self): + msg = ('Unable to parse debian/control, paragraph 2 missing Package ' + 'field') + with self.assertRaisesRegex(Exception, msg): + DebHelper(self.build_options()) + + +class TestRemainingPackages(DebHelperTestCase): + control = CONTROL + options = { + 'remaining_packages': True, + } + parse_control = False + + def setUp(self): + super().setUp() + with open('debian/python3-foo.debhelper.log', 'w') as f: + f.write('dh_python3\n') + with open('debian/python3-foo-ext.debhelper.log', 'w') as f: + f.write('dh_foobar\n') + self.dh = DebHelper(self.build_options(), impl=self.impl) + + def test_skips_logged_packages(self): + self.assertEqual(list(self.dh.packages.keys()), + ['python3-foo-ext', 'foo', 'recfoo']) diff --git a/tests/test_depends.py b/tests/test_depends.py new file mode 100644 index 0000000..d208438 --- /dev/null +++ b/tests/test_depends.py @@ -0,0 +1,726 @@ +import os +import logging +import platform +import unittest +from copy import deepcopy +from pickle import dumps +from tempfile import TemporaryDirectory + +from dhpython.depends import Dependencies +from dhpython.version import Version + +from tests.common import FakeOptions + + +def pep386(d): + """Mark all pydist entries as being PEP386""" + for k, v in d.items(): + if isinstance(v, str): + d[k] = {'dependency': v} + d[k].setdefault('standard', 'PEP386') + return d + + +def py27(d): + """Mark all pydist entries as being for Python 2.7""" + for k, v in d.items(): + if isinstance(v, str): + d[k] = {'dependency': v} + d[k].setdefault('versions', {Version('2.7')}) + return d + + +def prime_pydist(impl, pydist): + """Fake the pydist data for impl. Returns a cleanup function""" + from dhpython.pydist import load + + for name, entries in pydist.items(): + if not isinstance(entries, list): + pydist[name] = entries = [entries] + for i, entry in enumerate(entries): + if isinstance(entry, str): + entries[i] = entry = {'dependency': entry} + entry.setdefault('name', name) + entry.setdefault('standard', '') + entry.setdefault('rules', []) + entry.setdefault('versions', set()) + + key = dumps(((impl,), {})) + load.cache[key] = pydist + return lambda: load.cache.pop(key) + + +class DependenciesTestCase(unittest.TestCase): + pkg = 'foo' + impl = 'cpython3' + pydist = {} + stats = { + 'compile': False, + 'dist-info': set(), + 'egg-info': set(), + 'ext_no_version': set(), + 'ext_vers': set(), + 'nsp.txt': set(), + 'private_dirs': {}, + 'public_vers': set(), + 'requires.txt': set(), + 'shebangs': set(), + } + requires = {} + dist_info_metadata = {} + options = FakeOptions() + parse = True + + def setUp(self): + self.d = Dependencies(self.pkg, self.impl) + + stats = deepcopy(self.stats) + write_files = {} + if self.requires: + for fn, lines in self.requires.items(): + write_files[fn] = lines + stats['requires.txt'].add(fn) + + if self.dist_info_metadata: + for fn, lines in self.dist_info_metadata.items(): + write_files[fn] = lines + stats['dist-info'].add(fn) + + if write_files: + self.tempdir = TemporaryDirectory() + self.addCleanup(self.tempdir.cleanup) + old_wd = os.getcwd() + os.chdir(self.tempdir.name) + self.addCleanup(os.chdir, old_wd) + + for fn, lines in write_files.items(): + os.makedirs(os.path.dirname(fn)) + with open(fn, 'w') as f: + f.write('\n'.join(lines)) + + cleanup = prime_pydist(self.impl, self.pydist) + self.addCleanup(cleanup) + + if self.parse: + self.d.parse(stats, self.options) + else: + self.prepared_stats = stats + + def assertNotInDepends(self, pkg): + """Assert that pkg doesn't appear *anywhere* in self.d.depends""" + for dep in self.d.depends: + for alt in dep.split('|'): + alt = alt.strip().split('(', 1)[0].strip() + if pkg == alt: + raise AssertionError(f'{pkg} appears in {alt}') + + +class TestRequiresCPython3(DependenciesTestCase): + options = FakeOptions(guess_deps=True) + pydist = { + 'bar': 'python3-bar', + 'baz': {'dependency': 'python3-baz', 'standard': 'PEP386'}, + 'quux': {'dependency': 'python3-quux', 'standard': 'PEP386'}, + } + requires = { + 'debian/foo/usr/lib/python3/dist-packages/foo.egg-info/requires.txt': ( + 'bar', + 'baz >= 1.0', + 'quux', + ), + } + + def test_depends_on_bar(self): + self.assertIn('python3-bar', self.d.depends) + + def test_depends_on_baz(self): + self.assertIn('python3-baz (>= 1.0)', self.d.depends) + + +class TestRequiresPyPy(DependenciesTestCase): + impl = 'pypy' + options = FakeOptions(guess_deps=True) + pydist = { + 'bar': 'pypy-bar', + 'baz': {'dependency': 'pypy-baz', 'standard': 'PEP386'}, + 'quux': {'dependency': 'pypy-quux', 'standard': 'PEP386'}, + } + requires = { + 'debian/foo/usr/lib/pypy/dist-packages/foo.egg-info/requires.txt': ( + 'bar', + 'baz >= 1.0', + 'quux', + ) + } + + def test_depends_on_bar(self): + self.assertIn('pypy-bar', self.d.depends) + + def test_depends_on_baz(self): + self.assertIn('pypy-baz (>= 1.0)', self.d.depends) + + +class TestRequiresCompatible(DependenciesTestCase): + options = FakeOptions(guess_deps=True) + pydist = { + 'bar': 'python3-bar', + 'baz': {'dependency': 'python3-baz', 'standard': 'PEP386'}, + 'qux': {'dependency': 'python3-qux', 'standard': 'PEP386'}, + 'quux': {'dependency': 'python3-quux', 'standard': 'PEP386'}, + } + requires = { + 'debian/foo/usr/lib/python3/dist-packages/foo.egg-info/requires.txt': ( + 'bar', + 'baz ~= 1.1', + 'qux == 1.*', + 'quux', + ), + } + + def test_depends_on_bar(self): + self.assertIn('python3-bar', self.d.depends) + + def test_depends_on_baz(self): + self.assertIn('python3-baz (>= 1.1), python3-baz (<< 2)', self.d.depends) + + def test_depends_on_qux(self): + self.assertIn('python3-qux (>= 1.0), python3-qux (<< 2)', self.d.depends) + + +class TestRequiresDistPython3(DependenciesTestCase): + options = FakeOptions(guess_deps=True) + pydist = { + 'bar': 'python3-bar', + 'baz': {'dependency': 'python3-baz', 'standard': 'PEP386'}, + 'qux': {'dependency': 'python3-qux', 'standard': 'PEP386'}, + 'quux': {'dependency': 'python3-quux', 'standard': 'PEP386'}, + } + dist_info_metadata = { + 'debian/foo/usr/lib/python3/dist-packages/foo.dist-info/METADATA': ( + 'Requires-Dist: bar', + 'Requires-Dist: baz >= 1.0', + 'Requires-Dist: qux == 1.*', + 'Requires-Dist: quux ~= 1.1', + ), + } + + def test_depends_on_bar(self): + self.assertIn('python3-bar', self.d.depends) + + def test_depends_on_baz(self): + self.assertIn('python3-baz (>= 1.0)', self.d.depends) + + def test_depends_on_qux(self): + self.assertIn('python3-qux (>= 1.0), python3-qux (<< 2)', + self.d.depends) + + def test_depends_on_quux(self): + self.assertIn('python3-quux (>= 1.1), python3-quux (<< 2)', + self.d.depends) + + +class TestEnvironmentMarkersDistInfo(DependenciesTestCase): + options = FakeOptions(guess_deps=True, depends_section=['feature']) + pydist = pep386({ + 'no_markers': 'python3-no-markers', + 'os_posix': 'python3-os-posix', + 'os_java': 'python3-os-java', + 'sys_platform_linux': 'python3-sys-platform-linux', + 'sys_platform_darwin': 'python3-sys-platform-darwin', + 'platform_machine_x86_64': 'python3-platform-machine-x86-64', + 'platform_machine_mips64': 'python3-platform-machine-mips64', + 'platform_python_implementation_cpython': + 'python3-platform-python-implementation-cpython', + 'platform_python_implementation_jython': + 'python3-platform-python-implementation-jython', + 'platform_release_lt2': 'python3-platform-release-lt2', + 'platform_release_ge2': 'python3-platform-release-ge2', + 'platform_system_linux': 'python3-platform-system-linux', + 'platform_system_windows': 'python3-platform-system-windows', + 'platform_version_lt1': 'python3-platform-version-lt1', + 'platform_version_ge1': 'python3-platform-version-ge1', + 'python_version_ge3': 'python3-python-version-ge3', + 'python_version_gt3': 'python3-python-version-gt3', + 'python_version_lt3': 'python3-python-version-lt3', + 'python_version_lt30': 'python3-python-version-lt30', + 'python_version_lt35': 'python3-python-version-lt35', + 'python_version_le35': 'python3-python-version-le35', + 'python_version_ge27': 'python3-python-version-ge27', + 'python_version_ge35': 'python3-python-version-ge35', + 'python_version_gt35': 'python3-python-version-gt35', + 'python_version_eq35': 'python3-python-version-eq35', + 'python_version_ne35': 'python3-python-version-ne35', + 'python_version_aeq35': 'python3-python-version-aeq35', + 'python_version_ceq35': 'python3-python-version-ceq35', + 'python_version_weq35': 'python3-python-version-weq35', + 'python_version_full_lt300': 'python3-python-version-full-lt300', + 'python_version_full_lt351': 'python3-python-version-full-lt351', + 'python_version_full_le351': 'python3-python-version-full-le351', + 'python_version_full_ge351': 'python3-python-version-full-ge351', + 'python_version_full_ge351a1': 'python3-python-version-full-ge351a1', + 'python_version_full_ge351b1post1': + 'python3-python-version-full-ge351b1post1', + 'python_version_full_gt351': 'python3-python-version-full-gt351', + 'python_version_full_eq351': 'python3-python-version-full-eq351', + 'python_version_full_ne351': 'python3-python-version-full-ne351', + 'python_version_full_aeq351': 'python3-python-version-full-aeq351', + 'python_version_full_ceq351': 'python3-python-version-full-ceq351', + 'python_version_full_weq35': 'python3-python-version-full-weq35', + 'implementation_name_cpython': 'python3-implementation-name-cpython', + 'implementation_name_pypy': 'python3-implementation-name-pypy', + 'implementation_version_lt35': 'python3-implementation-version-lt35', + 'implementation_version_ge35': 'python3-implementation-version-ge35', + 'invalid_marker': 'python3-invalid-marker', + 'extra_feature': 'python3-extra-feature', + 'extra_test': 'python3-extra-test', + 'complex_marker': 'python3-complex-marker', + 'complex_marker_2': 'python3-complex-marker-2', + 'no_markers_2': 'python3-no-markers-2', + }) + dist_info_metadata = { + 'debian/foo/usr/lib/python3/dist-packages/foo.dist-info/METADATA': ( + "Requires-Dist: no_markers", + "Requires-Dist: os_posix; (os_name == 'posix')", + 'Requires-Dist: os_java; os_name == "java"', + "Requires-Dist: sys_platform_linux ; sys_platform == 'linux'", + "Requires-Dist: sys_platform_darwin;sys_platform == 'darwin'", + "Requires-Dist: platform_machine_x86_64; " + "platform_machine == 'x86_64'", + "Requires-Dist: platform_machine_mips64; " + "platform_machine == 'mips64'", + "Requires-Dist: platform_python_implementation_cpython; " + "platform_python_implementation == 'CPython'", + "Requires-Dist: platform_python_implementation_jython; " + "platform_python_implementation == 'Jython'", + "Requires-Dist: platform_release_lt2; platform_release < '2.0'", + "Requires-Dist: platform_release_ge2; platform_release >= '2.0'", + "Requires-Dist: platform_system_linux; platform_system == 'Linux'", + "Requires-Dist: platform_system_windows; " + "platform_system == 'Windows'", + "Requires-Dist: platform_version_lt1; platform_version < '1'", + "Requires-Dist: platform_version_ge1; platform_version >= '1'", + "Requires-Dist: python_version_ge3; python_version >= '3'", + "Requires-Dist: python_version_gt3; python_version > '3'", + "Requires-Dist: python_version_lt3; python_version < '3'", + "Requires-Dist: python_version_lt30; python_version < '3.0'", + "Requires-Dist: python_version_lt35; python_version < '3.5'", + "Requires-Dist: python_version_le35; python_version <= '3.5'", + "Requires-Dist: python_version_gt35; python_version > '3.5'", + "Requires-Dist: python_version_ge27; python_version >= '2.7'", + "Requires-Dist: python_version_ge35; python_version >= '3.5'", + "Requires-Dist: python_version_eq35; python_version == '3.5'", + "Requires-Dist: python_version_ne35; python_version != '3.5'", + "Requires-Dist: python_version_aeq35; python_version === '3.5'", + "Requires-Dist: python_version_ceq35; python_version ~= '3.5'", + "Requires-Dist: python_version_weq35; python_version == '3.5.*'", + "Requires-Dist: python_version_full_lt300; " + "python_full_version < '3.0.0'", + "Requires-Dist: python_version_full_lt351; " + "python_full_version < '3.5.1'", + "Requires-Dist: python_version_full_le351; " + "python_full_version <= '3.5.1'", + "Requires-Dist: python_version_full_gt351; " + "python_full_version > '3.5.1'", + "Requires-Dist: python_version_full_ge351; " + "python_full_version >= '3.5.1'", + "Requires-Dist: python_version_full_ge351a1; " + "python_full_version >= '3.5.1a1'", + "Requires-Dist: python_version_full_ge351b1post1; " + "python_full_version >= '3.5.1b1.post1'", + "Requires-Dist: python_version_full_eq351; " + "python_full_version == '3.5.1'", + "Requires-Dist: python_version_full_ne351; " + "python_full_version != '3.5.1'", + "Requires-Dist: python_version_full_aeq351; " + "python_full_version === '3.5.1'", + "Requires-Dist: python_version_full_ceq351; " + "python_full_version ~= '3.5.1'", + "Requires-Dist: python_version_full_weq35; " + "python_full_version == '3.5.*'", + "Requires-Dist: implementation_name_cpython; " + "implementation_name == 'cpython'", + "Requires-Dist: implementation_name_pypy; " + "implementation_name == 'pypy'", + "Requires-Dist: implementation_version_lt35; " + "implementation_version < '3.5'", + "Requires-Dist: implementation_version_ge35; " + "implementation_version >= '3.5'", + "Requires-Dist: invalid_marker; invalid_marker > '1'", + "Requires-Dist: extra_feature; extra == 'feature'", + "Requires-Dist: extra_test; extra == 'test'", + "Requires-Dist: complex_marker; os_name != 'windows' " + "and implementation_name == 'cpython'", + "Requires-Dist: complex_marker_2; (python_version > \"3.4\") " + "and extra == 'test'", + "Requires-Dist: no_markers_2", + ), + } + + def test_depends_on_unmarked_packages(self): + self.assertIn('python3-no-markers', self.d.depends) + + def test_depends_on_posix_packages(self): + self.assertIn('python3-os-posix', self.d.depends) + + def test_skips_non_posix_packages(self): + self.assertNotInDepends('python3-os-java') + + def test_depends_on_linux_packages(self): + self.assertIn('python3-sys-platform-linux', self.d.depends) + + def test_skips_darwin_packages(self): + self.assertNotInDepends('python3-sys-platform-darwin') + + def test_depends_on_x86_64_packages_on_x86_64(self): + if platform.machine() == 'x86_64': + self.assertIn('python3-platform-machine-x86-64', self.d.depends) + else: + self.assertNotInDepends('python3-platform-machine-x86-64') + + def test_depends_on_mips64_packages_on_mips64(self): + if platform.machine() == 'mips64': + self.assertIn('python3-platform-machine-mips64', self.d.depends) + else: + self.assertNotInDepends('python3-platform-machine-mips64') + + def test_depends_on_plat_cpython_packages(self): + self.assertIn('python3-platform-python-implementation-cpython', + self.d.depends) + + def test_skips_plat_jython_packages(self): + self.assertNotInDepends('python3-platform-python-implementation-jython') + + def test_skips_release_lt_2_packages(self): + self.assertNotInDepends('python3-platform-release-lt2') + + def test_skips_release_gt_2_packages(self): + self.assertNotInDepends('python3-platform-release-ge2') + + def test_depends_on_platform_linux_packages(self): + self.assertIn('python3-platform-system-linux', self.d.depends) + + def test_skips_platform_windows_packages(self): + self.assertNotInDepends('python3-platform-system-windows') + + def test_skips_platfrom_version_lt_1_packages(self): + self.assertNotInDepends('python3-platform-version-lt1') + + def test_skips_platform_version_ge_1_packages(self): + self.assertNotInDepends('python3-platform-version-ge1') + + def test_skips_py_version_lt_3_packages(self): + self.assertNotInDepends('python3-python-version-lt3') + + def test_elides_py_version_ge_3(self): + self.assertIn('python3-python-version-ge3', self.d.depends) + + def test_elides_py_version_gt_3(self): + self.assertIn('python3-python-version-gt3', self.d.depends) + + def test_skips_py_version_lt_30_packages(self): + self.assertNotInDepends('python3-python-version-lt30') + + def test_depends_on_py_version_lt_35_packages(self): + self.assertIn('python3-python-version-lt35 | python3 (>> 3.5)', + self.d.depends) + + def test_depends_on_py_version_le_35_packages(self): + self.assertIn('python3-python-version-le35 | python3 (>> 3.6)', + self.d.depends) + + def test_depends_on_py_version_ge_27_packages(self): + self.assertIn('python3-python-version-ge27', + self.d.depends) + + def test_depends_on_py_version_ge_35_packages(self): + self.assertIn('python3-python-version-ge35 | python3 (<< 3.5)', + self.d.depends) + + def test_depends_on_py_version_gt_35_packages(self): + self.assertIn('python3-python-version-gt35 | python3 (<< 3.6)', + self.d.depends) + + def test_depends_on_py_version_eq_35_packages(self): + self.assertIn('python3-python-version-eq35 | python3 (<< 3.5) ' + '| python3 (>> 3.6)', self.d.depends) + + def test_depends_on_py_version_ne_35_packages(self): + # Can't be represented in Debian depends + self.assertIn('python3-python-version-ne35', self.d.depends) + + def test_depends_on_py_version_aeq_35_packages(self): + self.assertIn('python3-python-version-aeq35 | python3 (<< 3.5) ' + '| python3 (>> 3.6)', self.d.depends) + + def test_depends_on_py_version_ceq_35_packages(self): + self.assertIn('python3-python-version-ceq35 | python3 (<< 3.5) ' + '| python3 (>> 3.6)', self.d.depends) + + def test_depends_on_py_version_weq_35_packages(self): + self.assertIn('python3-python-version-weq35 | python3 (<< 3.5) ' + '| python3 (>> 3.6)', self.d.depends) + + def test_skips_py_version_full_lt_300_packages(self): + self.assertNotInDepends('python3-python-version-full-lt300') + + def test_depends_on_py_version_full_lt_351_packages(self): + self.assertIn('python3-python-version-full-lt351 | python3 (>> 3.5.1)', + self.d.depends) + + def test_depends_on_py_version_full_le_351_packages(self): + self.assertIn('python3-python-version-full-le351 | python3 (>> 3.5.2)', + self.d.depends) + + def test_depends_on_py_version_full_ge_351_packages(self): + self.assertIn('python3-python-version-full-ge351 | python3 (<< 3.5.1)', + self.d.depends) + + def test_depends_on_py_version_full_ge_351a1_packages(self): + # With full PEP-440 parsing this should be (<< 3.5.1~a1) + self.assertIn( + 'python3-python-version-full-ge351a1 | python3 (<< 3.5.0)', + self.d.depends) + + def test_depends_on_py_version_full_ge_351b1post1_packages(self): + # With full PEP-440 parsing this should be (<< 3.5.1~b1.post1) + self.assertIn('python3-python-version-full-ge351a1 ' + '| python3 (<< 3.5.0)', + self.d.depends) + + def test_depends_on_py_version_full_gt_351_packages(self): + self.assertIn('python3-python-version-full-gt351 | python3 (<< 3.5.2)', + self.d.depends) + + def test_depends_on_py_version_full_eq_351_packages(self): + self.assertIn('python3-python-version-full-eq351 | python3 (<< 3.5.1) ' + '| python3 (>> 3.5.2)', self.d.depends) + + def test_depends_on_py_version_full_ne_351_packages(self): + # Can't be represented in Debian depends + self.assertIn('python3-python-version-full-ne351', self.d.depends) + + def test_skips_py_version_full_aeq_351_packages(self): + # Can't be represented in Debian depends + self.assertNotInDepends('python3-python-version-full-aeq351') + + def test_depends_on_py_version_full_ceq_351_packages(self): + self.assertIn('python3-python-version-full-ceq351 | python3 (<< 3.5.1) ' + '| python3 (>> 3.6)', self.d.depends) + + def test_depends_on_py_version_full_weq_35_packages(self): + self.assertIn('python3-python-version-full-weq35 | python3 (<< 3.5) ' + '| python3 (>> 3.6)', self.d.depends) + + def test_depends_on_sys_cpython_packages(self): + self.assertIn('python3-implementation-name-cpython', self.d.depends) + + def test_depends_on_sys_pypy_packages(self): + self.assertIn('python3-implementation-name-pypy', self.d.depends) + + def test_depends_on_sys_implementation_lt35_packages(self): + self.assertIn('python3-implementation-version-lt35 | python3 (>> 3.5)', + self.d.depends) + + def test_depends_on_sys_implementation_ge35_packages(self): + self.assertIn('python3-implementation-version-ge35 | python3 (<< 3.5)', + self.d.depends) + + def test_ignores_invalid_marker(self): + self.assertNotInDepends('python3-invalid-marker') + + def test_depends_on_extra_feature_packages(self): + self.assertIn('python3-extra-feature', self.d.depends) + + def test_skips_extra_test_packages(self): + self.assertNotInDepends('python3-extra-test') + + def test_ignores_complex_environment_markers(self): + self.assertNotInDepends('python3-complex-marker') + self.assertNotInDepends('python3-complex-marker-2') + + def test_depends_on_un_marked_dependency_after_extra(self): + self.assertIn('python3-no-markers-2', self.d.depends) + + +class TestEnvironmentMarkersEggInfo(TestEnvironmentMarkersDistInfo): + dist_info_metadata = None + requires = { + 'debian/foo/usr/lib/python3/dist-packages/foo.egg-info/requires.txt': ( + "no_markers", + "[:(os_name == 'posix')]", + "os_posix", + '[:os_name == "java"]', + "os_java", + "[:sys_platform == 'linux']", + "sys_platform_linux", + "[:sys_platform == 'darwin']", + "sys_platform_darwin", + "[:platform_machine == 'x86_64']", + "platform_machine_x86_64", + "[:platform_machine == 'mips64']", + "platform_machine_mips64", + "[:platform_python_implementation == 'CPython']", + "platform_python_implementation_cpython", + "[:platform_python_implementation == 'Jython']", + "platform_python_implementation_jython", + "[:platform_release < '2.0']", + "platform_release_lt2", + "[:platform_release >= '2.0']", + "platform_release_ge2", + "[:platform_system == 'Linux']", + "platform_system_linux", + "[:platform_system == 'Windows']", + "platform_system_windows", + "[:platform_version < '1']", + "platform_version_lt1", + "[:platform_version >= '1']", + "platform_version_ge1", + "[:python_version >= '3']", + "python_version_ge3", + "[:python_version > '3']", + "python_version_gt3", + "[:python_version < '3']", + "python_version_lt3", + "[:python_version < '3.0']", + "python_version_lt30", + "[:python_version < '3.5']", + "python_version_lt35", + "[:python_version <= '3.5']", + "python_version_le35", + "[:python_version > '3.5']", + "python_version_gt35", + "[:python_version >= '2.7']", + "python_version_ge27", + "[:python_version >= '3.5']", + "python_version_ge35", + "[:python_version == '3.5']", + "python_version_eq35", + "[:python_version != '3.5']", + "python_version_ne35", + "[:python_version === '3.5']", + "python_version_aeq35", + "[:python_version ~= '3.5']", + "python_version_ceq35", + "[:python_version == '3.5.*']", + "python_version_weq35", + "[:python_full_version < '3.0.0']", + "python_version_full_lt300", + "[:python_full_version < '3.5.1']", + "python_version_full_lt351", + "[:python_full_version <= '3.5.1']", + "python_version_full_le351", + "[:python_full_version > '3.5.1']", + "python_version_full_gt351", + "[:python_full_version >= '3.5.1']", + "python_version_full_ge351", + "[:python_full_version >= '3.5.1a1']", + "python_version_full_ge351a1", + "[:python_full_version >= '3.5.1b1.post1']", + "python_version_full_ge351b1post1", + "[:python_full_version == '3.5.1']", + "python_version_full_eq351", + "[:python_full_version != '3.5.1']", + "python_version_full_ne351", + "[:python_full_version === '3.5.1']", + "python_version_full_aeq351", + "[:python_full_version ~= '3.5.1']", + "python_version_full_ceq351", + "[:python_full_version == '3.5.*']", + "python_version_full_weq35", + "[:implementation_name == 'cpython']", + "implementation_name_cpython", + "[:implementation_name == 'pypy']", + "implementation_name_pypy", + "[:implementation_version < '3.5']", + "implementation_version_lt35", + "[:implementation_version >= '3.5']", + "implementation_version_ge35", + "[:invalid_marker > '1']", + "invalid_marker", + "[feature]", + "extra_feature", + "[test]", + "extra_test", + "[:os_name != 'windows' and implementation_name == 'cpython']", + "complex_marker", + "[test:(os_name != 'windows')]", + "complex_marker_2", + ), + } + + def test_depends_on_un_marked_dependency_after_extra(self): + raise unittest.SkipTest('Not possible in requires.txt') + + +class TestEnvironmentMarkers27EggInfo(DependenciesTestCase): + options = FakeOptions(guess_deps=True) + impl = 'cpython2' + requires = { + 'debian/foo/usr/lib/python2.7/dist-packages/foo.egg-info/requires.txt': ( + "no_markers", + "[:os_name == 'posix']", + "os_posix", + "[:python_version >= '2.6']", + "python_version_ge26", + ) + } + pydist = py27({ + 'no_markers': 'python-no-markers', + 'os_posix': 'python-os-posix', + 'python_version_ge26': 'python-python-version-ge26', + }) + + def test_depends_on_unmarked_packages(self): + self.assertIn('python-no-markers', self.d.depends) + + def test_ignores_posix_packages(self): + self.assertNotInDepends('python-os-posix') + + def test_ignores_pyversion_packages(self): + self.assertNotInDepends('python-python-version-ge26') + + +class TestIgnoresUnusedModulesDistInfo(DependenciesTestCase): + options = FakeOptions(guess_deps=True, depends_section=['feature']) + dist_info_metadata = { + 'debian/foo/usr/lib/python3/dist-packages/foo.dist-info/METADATA': ( + "Requires-Dist: unusued-complex-module ; " + "(sys_platform == \"darwin\") and extra == 'nativelib'", + "Requires-Dist: unused-win-module ; (sys_platform == \"win32\")", + "Requires-Dist: unused-extra-module ; extra == 'unused'", + ), + } + parse = False + + def test_ignores_unused_dependencies(self): + if not hasattr(self, 'assertLogs'): + raise unittest.SkipTest("Requires Python >= 3.4") + with self.assertLogs(logger='dhpython', level=logging.INFO) as logs: + self.d.parse(self.prepared_stats, self.options) + for line in logs.output: + self.assertTrue( + line.startswith( + 'INFO:dhpython:Ignoring complex environment marker'), + 'Expecting only complex environment marker messages, but ' + 'got: {}'.format(line)) + + +class TestIgnoresUnusedModulesEggInfo(DependenciesTestCase): + options = FakeOptions(guess_deps=True, depends_section=['feature']) + requires = { + 'debian/foo/usr/lib/python3/dist-packages/foo.egg-info/requires.txt': ( + "[nativelib:(sys_platform == 'darwin')]", + "unusued-complex-module", + "[:sys_platform == 'win32']", + "unused-win-module", + "[unused]", + "unused-extra-module", + ) + } + parse = False + + def test_ignores_unused_dependencies(self): + if not hasattr(self, 'assertNoLogs'): + raise unittest.SkipTest("Requires Python >= 3.10") + with self.assertNoLogs(logger='dhpython', level=logging.INFO): + self.d.parse(self.prepared_stats, self.options) diff --git a/tests/test_fs.py b/tests/test_fs.py new file mode 100644 index 0000000..c458fe8 --- /dev/null +++ b/tests/test_fs.py @@ -0,0 +1,145 @@ +from tempfile import TemporaryDirectory +from pathlib import Path +from unittest import TestCase + +from dhpython.interpreter import Interpreter +from dhpython.fs import ( + fix_merged_RECORD, merge_RECORD, merge_WHEEL, missing_lines, share_files) + +from tests.common import FakeOptions + + +class MergeWheelTestCase(TestCase): + files = {} + def setUp(self): + self.tempdir = TemporaryDirectory() + self.addCleanup(self.tempdir.cleanup) + temp_path = Path(self.tempdir.name) + for fn, contents in self.files.items(): + path = temp_path / fn + setattr(self, path.name, path) + path.parent.mkdir(parents=True, exist_ok=True) + with path.open('w') as f: + f.write('\n'.join(contents)) + f.write('\n') + + def assertFileContents(self, path, contents): + """Assert that the contents of path is contents + + Contents may be specified as a list of strings, one per line, without + line-breaks. + """ + if isinstance(contents, (list, tuple)): + contents = '\n'.join(contents) + '\n' + with path.open('r') as f: + self.assertMultiLineEqual(contents, f.read()) + + +class SimpleCombinationTest(MergeWheelTestCase): + files = { + 'a': ('abc', 'def'), + 'b': ('abc', 'ghi'), + } + def test_missing_lines(self): + r = missing_lines(self.a, self.b) + self.assertEqual(r, ['def\n']) + + def test_merge_record(self): + merge_RECORD(self.a, self.b) + self.assertFileContents(self.b, ('abc', 'ghi', 'def')) + + +class MergeTagsTest(MergeWheelTestCase): + files = { + 'a': ('foo', 'Tag: A'), + 'b': ('foo', 'Tag: B'), + } + + def test_merge_wheel(self): + merge_WHEEL(self.a, self.b) + self.assertFileContents(self.b, ('foo', 'Tag: B', 'Tag: A')) + + +class UpdateRecordTest(MergeWheelTestCase): + files = { + 'dist-info/RECORD': ('dist-info/FOO,sha256=b5bb9d8014a0f9b1d61e21e796d7' + '8dccdf1352f23cd32812f4850b878ae4944c,4',), + 'dist-info/WHEEL': ('foo'), + } + + def test_fix_merged_record(self): + fix_merged_RECORD(self.RECORD.parent) + self.assertFileContents(self.RECORD, ( + 'dist-info/FOO,sha256=b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32' + '812f4850b878ae4944c,4', + 'dist-info/WHEEL,sha256=447fb61fa39a067229e1cce8fc0953bfced53eac85d' + '1844f5940f51c1fcba725,6', + )) + + +class ShareFilesTestCase(MergeWheelTestCase): + impl = 'cpython3' + options = {} + + def setUp(self): + super().setUp() + self.destdir = TemporaryDirectory() + self.addCleanup(self.destdir.cleanup) + share_files(self.tempdir.name, self.destdir.name, + Interpreter(self.impl), + FakeOptions(**self.options)) + + def destPath(self, name): + return Path(self.destdir.name) / name + + +class HatchlingLicenseTest(ShareFilesTestCase): + files = { + 'foo.dist-info/license_files/LICENSE.txt': ('foo'), + 'foo.dist-info/licenses/COPYING': ('foo'), + 'foo.dist-info/RECORD': ( + 'foo.dist-info/license_files/LICENSE.txt,sha256=2c26b46b68ffc68ff99' + 'b453c1d30413413422d706483bfa0f98a5e886266e7ae,4', + 'foo.dist-info/licenses/COPYING,sha256=2c26b46b68ffc68ff99b453c1d30' + '413413422d706483bfa0f98a5e886266e7ae,4', + 'foo.dist-info/WHEEL,sha256=447fb61fa39a067229e1cce8fc0953bfced53ea' + 'c85d1844f5940f51c1fcba725,6'), + 'foo.dist-info/WHEEL': ('foo'), + } + + def test_removes_license_files(self): + self.assertFalse( + self.destPath('foo.dist-info/license_files/LICENSE.txt').exists()) + self.assertFalse( + self.destPath('foo.dist-info/licenses/COPYING').exists()) + + def test_removes_license_files_from_record(self): + print("Checking", self.destPath('foo.dist-info/RECORD')) + self.assertFileContents(self.destPath('foo.dist-info/RECORD'), + 'foo.dist-info/WHEEL,sha256=447fb61fa39a067229e1cce8fc0953bfced53ea' + 'c85d1844f5940f51c1fcba725,6\n') + + +class FlitLicenseTest(ShareFilesTestCase): + files = { + 'foo.dist-info/COPYING': ('foo'), + 'foo.dist-info/COPYING.LESSER': ('foo'), + 'foo.dist-info/RECORD': ( + 'foo.dist-info/COPYING,sha256=2c26b46b68ffc68ff99b453c1d30413413422' + 'd706483bfa0f98a5e886266e7ae,4', + 'foo.dist-info/COPYING.LESSER,sha256=2c26b46b68ffc68ff99b453c1d3041' + '3413422d706483bfa0f98a5e886266e7ae,4', + 'foo.dist-info/WHEEL,sha256=447fb61fa39a067229e1cce8fc0953bfced53ea' + 'c85d1844f5940f51c1fcba725,6'), + 'foo.dist-info/WHEEL': ('foo'), + } + + def test_removes_license_files(self): + self.assertFalse(self.destPath('foo.dist-info/COPYING.LESSER').exists()) + self.assertFalse(self.destPath('foo.dist-info/COPYING').exists()) + + def test_removes_license_files_from_record(self): + print("Checking", self.destPath('foo.dist-info/RECORD')) + self.assertFileContents(self.destPath('foo.dist-info/RECORD'), + 'foo.dist-info/WHEEL,sha256=447fb61fa39a067229e1cce8fc0953bfced53ea' + 'c85d1844f5940f51c1fcba725,6\n') diff --git a/tests/test_interpreter.py b/tests/test_interpreter.py new file mode 100644 index 0000000..ff9bdd2 --- /dev/null +++ b/tests/test_interpreter.py @@ -0,0 +1,250 @@ +import unittest +from os import environ +from os.path import exists +from dhpython.interpreter import Interpreter + + +class TestInterpreter(unittest.TestCase): + def setUp(self): + self._triplet = environ.get('DEB_HOST_MULTIARCH') + environ['DEB_HOST_MULTIARCH'] = 'MYARCH' + + def tearDown(self): + if self._triplet: + environ['DEB_HOST_MULTIARCH'] = self._triplet + + @unittest.skipUnless(exists('/usr/bin/pypy'), 'pypy is not installed') + def test_pypy(self): + sorted(Interpreter.parse('pypy').items()) == \ + [('debug', None), ('name', 'pypy'), ('options', ()), ('path', ''), ('version', None)] + sorted(Interpreter.parse('#! /usr/bin/pypy --foo').items()) == \ + [('debug', None), ('name', 'pypy'), ('options', ('--foo',)), ('path', '/usr/bin/'), ('version', None)] + Interpreter('pypy').sitedir(version='2.0') == '/usr/lib/pypy/dist-packages/' + + @unittest.skipUnless(exists('/usr/bin/python2.6'), 'python2.6 is not installed') + def test_python26(self): + i = Interpreter('python2.6') + self.assertEqual(i.soabi(), '') + self.assertIsNone(i.check_extname('foo.so')) + self.assertIsNone(i.check_extname('foo.abi3.so')) + self.assertIsNone(i.check_extname('foo/bar/bazmodule.so')) + + @unittest.skipUnless(exists('/usr/bin/python2.6-dbg'), 'python2.6-dbg is not installed') + def test_python26dbg(self): + i = Interpreter('python2.6-dbg') + self.assertEqual(i.soabi(), '') + self.assertIsNone(i.check_extname('foo_d.so')) + self.assertEqual(i.check_extname('foo.so'), 'foo_d.so') + self.assertEqual(i.check_extname('foo/bar/bazmodule.so'), 'foo/bar/bazmodule_d.so') + + @unittest.skipUnless(exists('/usr/bin/python2.7'), 'python2.7 is not installed') + def test_python27(self): + i = Interpreter('python2.7') + self.assertEqual(i.soabi(), '') + self.assertEqual(i.check_extname('foo.so'), 'foo.MYARCH.so') + self.assertIsNone(i.check_extname('foo.MYARCH_d.so')) + self.assertIsNone(i.check_extname('foo.abi3.so')) + self.assertIsNone(i.check_extname('foo.OTHER.so')) # different architecture + self.assertEqual(i.check_extname('foo/bar/bazmodule.so'), 'foo/bar/baz.MYARCH.so') + + @unittest.skipUnless(exists('/usr/bin/python2.7-dbg'), 'python2.7-dbg is not installed') + def test_python27dbg(self): + i = Interpreter('python2.7-dbg') + self.assertEqual(i.soabi(), '') + self.assertEqual(i.check_extname('foo.so'), 'foo.MYARCH_d.so') + self.assertEqual(i.check_extname('foo_d.so'), 'foo.MYARCH_d.so') + self.assertIsNone(i.check_extname('foo.MYARCH_d.so')) + self.assertIsNone(i.check_extname('foo.OTHER_d.so')) # different architecture + self.assertEqual(i.check_extname('foo/bar/bazmodule.so'), 'foo/bar/baz.MYARCH_d.so') + + @unittest.skipUnless(exists('/usr/bin/python3.1'), 'python3.1 is not installed') + def test_python31(self): + i = Interpreter('python3.1') + self.assertEqual(i.soabi(), '') + self.assertIsNone(i.check_extname('foo.so')) + self.assertIsNone(i.check_extname('foo.abi3.so')) + self.assertIsNone(i.check_extname('foo/bar/bazmodule.so')) + + @unittest.skipUnless(exists('/usr/bin/python3.1-dbg'), 'python3.1-dbg is not installed') + def test_python31dbg(self): + i = Interpreter('python3.1-dbg') + self.assertEqual(i.soabi(), '') + self.assertIsNone(i.check_extname('foo.so')) + self.assertIsNone(i.check_extname('foo.abi3.so')) + self.assertIsNone(i.check_extname('foo/bar/bazmodule.so')) + + @unittest.skipUnless(exists('/usr/bin/python3.2'), 'python3.2 is not installed') + def test_python32(self): + i = Interpreter('python3.2') + self.assertEqual(i.soabi(), 'cpython-32mu') + self.assertEqual(i.check_extname('foo.so'), r'foo.cpython-32mu.so') + self.assertIsNone(i.check_extname('foo.cpython-33m.so')) # different version + self.assertIsNone(i.check_extname('foo.cpython-32mu-OTHER.so')) # different architecture + self.assertIsNone(i.check_extname('foo.abi3.so')) + self.assertEqual(i.check_extname('foo/bar/bazmodule.so'), r'foo/bar/bazmodule.cpython-32mu.so') + + @unittest.skipUnless(exists('/usr/bin/python3.2-dbg'), 'python3.2-dbg is not installed') + def test_python32dbg(self): + i = Interpreter('python3.2-dbg') + self.assertEqual(i.soabi(), 'cpython-32dmu') + self.assertEqual(i.check_extname('foo.so'), r'foo.cpython-32dmu.so') + self.assertIsNone(i.check_extname('foo.cpython-33m.so')) # different version + self.assertIsNone(i.check_extname('foo.cpython-32dmu-OTHER.so')) # different architecture + self.assertIsNone(i.check_extname('foo.abi3.so')) + self.assertEqual(i.check_extname('foo/bar/bazmodule.so'), r'foo/bar/bazmodule.cpython-32dmu.so') + + @unittest.skipUnless(exists('/usr/bin/python3.4'), 'python3.4 is not installed') + def test_python34(self): + i = Interpreter('python3.4') + self.assertEqual(i.soabi(), 'cpython-34m') + self.assertEqual(i.check_extname('foo.so'), r'foo.cpython-34m-MYARCH.so') + self.assertIsNone(i.check_extname('foo.cpython-32m.so')) # different version + self.assertIsNone(i.check_extname('foo.cpython-34m-OTHER.so')) # different architecture + self.assertEqual(i.check_extname('foo.cpython-34m.so'), r'foo.cpython-34m-MYARCH.so') + self.assertIsNone(i.check_extname('foo.abi3.so')) + self.assertEqual(i.check_extname('foo/bar/bazmodule.so'), r'foo/bar/baz.cpython-34m-MYARCH.so') + + @unittest.skipUnless(exists('/usr/bin/python3.4-dbg'), 'python3.4-dbg is not installed') + def test_python34dbg(self): + i = Interpreter('python3.4-dbg') + self.assertEqual(i.soabi(), 'cpython-34dm') + self.assertEqual(i.check_extname('foo.so'), r'foo.cpython-34dm-MYARCH.so') + self.assertIsNone(i.check_extname('foo.cpython-32m.so')) # different version + self.assertIsNone(i.check_extname('foo.cpython-34m-OTHER.so')) # different architecture + self.assertIsNone(i.check_extname('foo.abi3.so')) + self.assertEqual(i.check_extname('foo/bar/bazmodule.so'), r'foo/bar/baz.cpython-34dm-MYARCH.so') + + @unittest.skipUnless(exists('/usr/bin/python3.5'), 'python3.5 is not installed') + def test_python35(self): + i = Interpreter('python3.5') + self.assertEqual(i.soabi(), 'cpython-35m') + self.assertEqual(i.check_extname('foo.so'), r'foo.cpython-35m-MYARCH.so') + self.assertIsNone(i.check_extname('foo.cpython-32m.so')) # different version + self.assertIsNone(i.check_extname('foo.cpython-35m-OTHER.so')) # different architecture + self.assertEqual(i.check_extname('foo.cpython-35m.so'), r'foo.cpython-35m-MYARCH.so') + self.assertIsNone(i.check_extname('foo.abi3.so')) + self.assertEqual(i.check_extname('foo/bar/bazmodule.so'), r'foo/bar/baz.cpython-35m-MYARCH.so') + + @unittest.skipUnless(exists('/usr/bin/python3.5-dbg'), 'python3.5-dbg is not installed') + def test_python35dbg(self): + i = Interpreter('python3.5-dbg') + self.assertEqual(i.soabi(), 'cpython-35dm') + self.assertEqual(i.check_extname('foo.so'), r'foo.cpython-35dm-MYARCH.so') + self.assertIsNone(i.check_extname('foo.cpython-32m.so')) # different version + self.assertIsNone(i.check_extname('foo.cpython-35m-OTHER.so')) # different architecture + self.assertIsNone(i.check_extname('foo.abi3.so')) + self.assertEqual(i.check_extname('foo/bar/bazmodule.so'), r'foo/bar/baz.cpython-35dm-MYARCH.so') + + @unittest.skipUnless(exists('/usr/bin/python3.6'), 'python3.6 is not installed') + def test_python36(self): + i = Interpreter('python3.6') + self.assertEqual(i.soabi(), 'cpython-36m') + self.assertEqual(i.check_extname('foo.so'), r'foo.cpython-36m-MYARCH.so') + self.assertIsNone(i.check_extname('foo.cpython-32m.so')) # different version + self.assertIsNone(i.check_extname('foo.cpython-36m-OTHER.so')) # different architecture + self.assertEqual(i.check_extname('foo.cpython-36m.so'), r'foo.cpython-36m-MYARCH.so') + self.assertIsNone(i.check_extname('foo.abi3.so')) + self.assertEqual(i.check_extname('foo/bar/bazmodule.so'), r'foo/bar/baz.cpython-36m-MYARCH.so') + + @unittest.skipUnless(exists('/usr/bin/python3.6-dbg'), 'python3.6-dbg is not installed') + def test_python36dbg(self): + i = Interpreter('python3.6-dbg') + self.assertEqual(i.soabi(), 'cpython-36dm') + self.assertEqual(i.check_extname('foo.so'), r'foo.cpython-36dm-MYARCH.so') + self.assertIsNone(i.check_extname('foo.cpython-32m.so')) # different version + self.assertIsNone(i.check_extname('foo.cpython-36m-OTHER.so')) # different architecture + self.assertIsNone(i.check_extname('foo.abi3.so')) + self.assertEqual(i.check_extname('foo/bar/bazmodule.so'), r'foo/bar/baz.cpython-36dm-MYARCH.so') + + @unittest.skipUnless(exists('/usr/bin/python3.7'), 'python3.7 is not installed') + def test_python37(self): + i = Interpreter('python3.7') + self.assertEqual(i.soabi(), 'cpython-37m') + self.assertEqual(i.check_extname('foo.so'), r'foo.cpython-37m-MYARCH.so') + self.assertIsNone(i.check_extname('foo.cpython-32m.so')) # different version + self.assertIsNone(i.check_extname('foo.cpython-37m-OTHER.so')) # different architecture + self.assertEqual(i.check_extname('foo.cpython-37m.so'), r'foo.cpython-37m-MYARCH.so') + self.assertIsNone(i.check_extname('foo.abi3.so')) + self.assertEqual(i.check_extname('foo/bar/bazmodule.so'), r'foo/bar/baz.cpython-37m-MYARCH.so') + + @unittest.skipUnless(exists('/usr/bin/python3.7-dbg'), 'python3.7-dbg is not installed') + def test_python37dbg(self): + i = Interpreter('python3.7-dbg') + self.assertEqual(i.soabi(), 'cpython-37dm') + self.assertEqual(i.check_extname('foo.so'), r'foo.cpython-37dm-MYARCH.so') + self.assertIsNone(i.check_extname('foo.cpython-32m.so')) # different version + self.assertIsNone(i.check_extname('foo.cpython-37m-OTHER.so')) # different architecture + self.assertIsNone(i.check_extname('foo.abi3.so')) + self.assertEqual(i.check_extname('foo/bar/bazmodule.so'), r'foo/bar/baz.cpython-37dm-MYARCH.so') + + @unittest.skipUnless(exists('/usr/bin/python3.8'), 'python3.8 is not installed') + def test_python38(self): + i = Interpreter('python3.8') + self.assertEqual(i.soabi(), 'cpython-38') + self.assertEqual(i.check_extname('foo.so'), r'foo.cpython-38-MYARCH.so') + self.assertIsNone(i.check_extname('foo.cpython-32m.so')) # different version + self.assertIsNone(i.check_extname('foo.cpython-38-OTHER.so')) # different architecture + self.assertEqual(i.check_extname('foo.cpython-38.so'), r'foo.cpython-38-MYARCH.so') + self.assertIsNone(i.check_extname('foo.abi3.so')) + self.assertEqual(i.check_extname('foo/bar/bazmodule.so'), r'foo/bar/baz.cpython-38-MYARCH.so') + + @unittest.skipUnless(exists('/usr/bin/python3.8-dbg'), 'python3.8-dbg is not installed') + def test_python38dbg(self): + i = Interpreter('python3.8-dbg') + self.assertEqual(i.soabi(), 'cpython-38d') + self.assertEqual(i.check_extname('foo.so'), r'foo.cpython-38d-MYARCH.so') + self.assertIsNone(i.check_extname('foo.cpython-32m.so')) # different version + self.assertIsNone(i.check_extname('foo.cpython-38-OTHER.so')) # different architecture + self.assertIsNone(i.check_extname('foo.abi3.so')) + self.assertEqual(i.check_extname('foo/bar/bazmodule.so'), r'foo/bar/baz.cpython-38d-MYARCH.so') + + @unittest.skipUnless(exists('/usr/bin/python3.9'), 'python3.9 is not installed') + def test_python39(self): + i = Interpreter('python3.9') + self.assertEqual(i.soabi(), 'cpython-39') + self.assertEqual(i.check_extname('foo.so'), r'foo.cpython-39-MYARCH.so') + self.assertIsNone(i.check_extname('foo.cpython-32m.so')) # different version + self.assertIsNone(i.check_extname('foo.cpython-39-OTHER.so')) # different architecture + self.assertEqual(i.check_extname('foo.cpython-39.so'), r'foo.cpython-39-MYARCH.so') + self.assertIsNone(i.check_extname('foo.abi3.so')) + self.assertEqual(i.check_extname('foo/bar/bazmodule.so'), r'foo/bar/baz.cpython-39-MYARCH.so') + + @unittest.skipUnless(exists('/usr/bin/python3.9-dbg'), 'python3.9-dbg is not installed') + def test_python39dbg(self): + i = Interpreter('python3.9-dbg') + self.assertEqual(i.soabi(), 'cpython-39d') + self.assertEqual(i.check_extname('foo.so'), r'foo.cpython-39d-MYARCH.so') + self.assertIsNone(i.check_extname('foo.cpython-32m.so')) # different version + self.assertIsNone(i.check_extname('foo.cpython-39-OTHER.so')) # different architecture + self.assertIsNone(i.check_extname('foo.abi3.so')) + self.assertEqual(i.check_extname('foo/bar/bazmodule.so'), r'foo/bar/baz.cpython-39d-MYARCH.so') + + @unittest.skipUnless(exists('/usr/bin/python3.10'), 'python3.10 is not installed') + def test_python310(self): + i = Interpreter('python3.10') + self.assertEqual(i.soabi(), 'cpython-310') + self.assertEqual(i.check_extname('foo.so'), r'foo.cpython-310-MYARCH.so') + self.assertIsNone(i.check_extname('foo.cpython-32m.so')) # different version + self.assertIsNone(i.check_extname('foo.cpython-310-OTHER.so')) # different architecture + self.assertEqual(i.check_extname('foo.cpython-310.so'), r'foo.cpython-310-MYARCH.so') + self.assertIsNone(i.check_extname('foo.abi3.so')) + self.assertEqual(i.check_extname('foo/bar/bazmodule.so'), r'foo/bar/baz.cpython-310-MYARCH.so') + + @unittest.skipUnless(exists('/usr/bin/python3.10-dbg'), 'python3.10-dbg is not installed') + def test_python310dbg(self): + i = Interpreter('python3.10-dbg') + self.assertEqual(i.soabi(), 'cpython-310d') + self.assertEqual(i.check_extname('foo.so'), r'foo.cpython-310d-MYARCH.so') + self.assertIsNone(i.check_extname('foo.cpython-32m.so')) # different version + self.assertIsNone(i.check_extname('foo.cpython-310-OTHER.so')) # different architecture + self.assertIsNone(i.check_extname('foo.abi3.so')) + self.assertEqual(i.check_extname('foo/bar/bazmodule.so'), r'foo/bar/baz.cpython-310d-MYARCH.so') + + + def test_version(self): + i = Interpreter(impl='cpython2') + self.assertEqual(str(i), 'python') + self.assertEqual(i.binary('2.7'), '/usr/bin/python2.7') + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_tools.py b/tests/test_tools.py new file mode 100644 index 0000000..55097e8 --- /dev/null +++ b/tests/test_tools.py @@ -0,0 +1,44 @@ +from tempfile import TemporaryDirectory +import os +import unittest + +from dhpython.tools import relpath, move_matching_files + + +class TestRelpath(unittest.TestCase): + def test_common_parent_dir(self): + r = relpath('/usr/share/python-foo/foo.py', '/usr/bin/foo') + self.assertEqual(r, '../share/python-foo/foo.py') + + def test_strips_common_prefix(self): + r = relpath('/usr/share/python-foo/foo.py', '/usr/share') + self.assertEqual(r, 'python-foo/foo.py') + + def test_trailing_slash_ignored(self): + r = relpath('/usr/share/python-foo/foo.py', '/usr/share/') + self.assertEqual(r, 'python-foo/foo.py') + + +class TestMoveMatchingFiles(unittest.TestCase): + def setUp(self): + self.tmpdir = TemporaryDirectory() + self.addCleanup(self.tmpdir.cleanup) + os.makedirs(self.tmppath('foo/bar/a/b/c/spam')) + for path in ('foo/bar/a/b/c/spam/file.so', + 'foo/bar/a/b/c/spam/file.py'): + open(self.tmppath(path), 'w').close() + + move_matching_files(self.tmppath('foo/bar/'), + self.tmppath('foo/baz/'), + 'spam/.*\.so$') + + def tmppath(self, *path): + return os.path.join(self.tmpdir.name, *path) + + def test_moved_matching_file(self): + self.assertTrue(os.path.exists( + self.tmppath('foo/baz/a/b/c/spam/file.so'))) + + def test_left_non_matching_file(self): + self.assertTrue(os.path.exists( + self.tmppath('foo/bar/a/b/c/spam/file.py'))) diff --git a/tests/tpb01/Makefile b/tests/tpb01/Makefile new file mode 100644 index 0000000..b33b698 --- /dev/null +++ b/tests/tpb01/Makefile @@ -0,0 +1,9 @@ +#!/usr/bin/make -f +include ../common.mk + +check: + test -f debian/python3-foo/usr/lib/python3/dist-packages/foo.py + grep -q 'Depends:.*python3-tomli' debian/python3-foo/DEBIAN/control + +clean: + ./debian/rules clean diff --git a/tests/tpb01/debian/changelog b/tests/tpb01/debian/changelog new file mode 100644 index 0000000..322011c --- /dev/null +++ b/tests/tpb01/debian/changelog @@ -0,0 +1,5 @@ +foo (1.2.3) unstable; urgency=low + + * Initial release + + -- Piotr Ozarowski <piotr@debian.org> Tue, 02 Jul 2013 11:02:06 +0200 diff --git a/tests/tpb01/debian/control b/tests/tpb01/debian/control new file mode 100644 index 0000000..8b2e05f --- /dev/null +++ b/tests/tpb01/debian/control @@ -0,0 +1,16 @@ +Source: foo +Section: python +Priority: optional +Maintainer: Piotr Ożarowski <piotr@debian.org> +Build-Depends: debhelper-compat (= 12) + , python3-all + , python3-setuptools + , python3-tomli +# , dh-python +Standards-Version: 3.9.4 + +Package: python3-foo +Architecture: all +Depends: ${python3:Depends}, ${shlibs:Depends}, ${misc:Depends} +Description: package with public CPython modules + example package #1 diff --git a/tests/tpb01/debian/copyright b/tests/tpb01/debian/copyright new file mode 100644 index 0000000..f96adde --- /dev/null +++ b/tests/tpb01/debian/copyright @@ -0,0 +1,2 @@ +The Debian packaging is © 2013, Piotr Ożarowski <piotr@debian.org> and +is licensed under the MIT License. diff --git a/tests/tpb01/debian/rules b/tests/tpb01/debian/rules new file mode 100755 index 0000000..e8953b0 --- /dev/null +++ b/tests/tpb01/debian/rules @@ -0,0 +1,26 @@ +#!/usr/bin/make -f + +export PYBUILD_NAME=foo + +%: + dh $@ + +override_dh_auto_build: + ../../pybuild --build + +override_dh_auto_install: + ../../pybuild --install + +override_dh_auto_test: + ../../pybuild --test + +override_dh_auto_clean: + ../../pybuild --clean + rm -rf .pybuild foo.egg-info + +override_dh_installinit: + DH_VERBOSE=1 ../../dh_python3 + dh_installinit + +override_dh_python3: + # ignore any system dh_python3 diff --git a/tests/tpb01/debian/source/format b/tests/tpb01/debian/source/format new file mode 100644 index 0000000..89ae9db --- /dev/null +++ b/tests/tpb01/debian/source/format @@ -0,0 +1 @@ +3.0 (native) diff --git a/tests/tpb01/foo.py b/tests/tpb01/foo.py new file mode 100644 index 0000000..810c96e --- /dev/null +++ b/tests/tpb01/foo.py @@ -0,0 +1 @@ +"foo" diff --git a/tests/tpb01/setup.cfg b/tests/tpb01/setup.cfg new file mode 100644 index 0000000..112fd6c --- /dev/null +++ b/tests/tpb01/setup.cfg @@ -0,0 +1,9 @@ +[metadata] +name = foo + +[options] +py_modules = foo +install_requires = + tomli + mock; python_version < '3.1' + docutils; python_version >= '3.0' diff --git a/tests/tpb01/setup.py b/tests/tpb01/setup.py new file mode 100644 index 0000000..6068493 --- /dev/null +++ b/tests/tpb01/setup.py @@ -0,0 +1,3 @@ +from setuptools import setup + +setup() diff --git a/tests/tpb02/Makefile b/tests/tpb02/Makefile new file mode 100644 index 0000000..3a86c66 --- /dev/null +++ b/tests/tpb02/Makefile @@ -0,0 +1,19 @@ +#!/usr/bin/make -f +include ../common.mk + +DI=debian/python3-foo/usr/lib/python3/dist-packages/foo-0.1.dist-info + +check: + test -f debian/python3-foo/usr/lib/python3/dist-packages/foo/__init__.py + test -f debian/python3-foo/usr/bin/foo + grep -q ^foo/__init__.py, $(DI)/RECORD + test ! -f $(DI)/direct_url.json + grep -L ^foo-0.1.dist-info/direct_url.json, $(DI)/RECORD | grep -q RECORD + grep -q 'Depends:.*python3-tomli' debian/python3-foo/DEBIAN/control + grep -q 'Depends:.*python3-importlib-metadata \| python3 (>> 3\.5)' debian/python3-foo/DEBIAN/control + grep -L 'Depends:.*tox' debian/python3-foo/DEBIAN/control | grep -q control + find .pybuild -name test-executed | grep -q test-executed + test -f debian/python3-foo/usr/share/man/man1/foo.1.gz + +clean: + ./debian/rules clean diff --git a/tests/tpb02/data/share/man/man1/foo.1 b/tests/tpb02/data/share/man/man1/foo.1 new file mode 100644 index 0000000..3bf5dce --- /dev/null +++ b/tests/tpb02/data/share/man/man1/foo.1 @@ -0,0 +1,8 @@ +.TH foo 1 "December 5 2022" +.SH NAME +foo \- An example +.SH SYNOPSIS +.B foo +.SH DESCRIPTION +.B foo +says hi. diff --git a/tests/tpb02/debian/changelog b/tests/tpb02/debian/changelog new file mode 100644 index 0000000..322011c --- /dev/null +++ b/tests/tpb02/debian/changelog @@ -0,0 +1,5 @@ +foo (1.2.3) unstable; urgency=low + + * Initial release + + -- Piotr Ozarowski <piotr@debian.org> Tue, 02 Jul 2013 11:02:06 +0200 diff --git a/tests/tpb02/debian/control b/tests/tpb02/debian/control new file mode 100644 index 0000000..c775ede --- /dev/null +++ b/tests/tpb02/debian/control @@ -0,0 +1,16 @@ +Source: foo +Section: python +Priority: optional +Maintainer: Piotr Ożarowski <piotr@debian.org> +Build-Depends: debhelper-compat (= 12) + , flit + , python3-all + , pybuild-plugin-pyproject +# , dh-python +Standards-Version: 3.9.4 + +Package: python3-foo +Architecture: all +Depends: ${python3:Depends}, ${shlibs:Depends}, ${misc:Depends} +Description: package with public CPython modules + example package #1 diff --git a/tests/tpb02/debian/copyright b/tests/tpb02/debian/copyright new file mode 100644 index 0000000..f96adde --- /dev/null +++ b/tests/tpb02/debian/copyright @@ -0,0 +1,2 @@ +The Debian packaging is © 2013, Piotr Ożarowski <piotr@debian.org> and +is licensed under the MIT License. diff --git a/tests/tpb02/debian/pybuild.testfiles b/tests/tpb02/debian/pybuild.testfiles new file mode 100644 index 0000000..58c4d14 --- /dev/null +++ b/tests/tpb02/debian/pybuild.testfiles @@ -0,0 +1,3 @@ +testfile1.txt +nested/testfile2.txt +testdir diff --git a/tests/tpb02/debian/rules b/tests/tpb02/debian/rules new file mode 100755 index 0000000..e8953b0 --- /dev/null +++ b/tests/tpb02/debian/rules @@ -0,0 +1,26 @@ +#!/usr/bin/make -f + +export PYBUILD_NAME=foo + +%: + dh $@ + +override_dh_auto_build: + ../../pybuild --build + +override_dh_auto_install: + ../../pybuild --install + +override_dh_auto_test: + ../../pybuild --test + +override_dh_auto_clean: + ../../pybuild --clean + rm -rf .pybuild foo.egg-info + +override_dh_installinit: + DH_VERBOSE=1 ../../dh_python3 + dh_installinit + +override_dh_python3: + # ignore any system dh_python3 diff --git a/tests/tpb02/debian/source/format b/tests/tpb02/debian/source/format new file mode 100644 index 0000000..89ae9db --- /dev/null +++ b/tests/tpb02/debian/source/format @@ -0,0 +1 @@ +3.0 (native) diff --git a/tests/tpb02/foo/__init__.py b/tests/tpb02/foo/__init__.py new file mode 100644 index 0000000..f29ed43 --- /dev/null +++ b/tests/tpb02/foo/__init__.py @@ -0,0 +1,6 @@ +"""An amazing sample package!""" + +__version__ = '0.1' + +def main(): + print("Hello There") diff --git a/tests/tpb02/foo/test_foo.py b/tests/tpb02/foo/test_foo.py new file mode 100644 index 0000000..174b86b --- /dev/null +++ b/tests/tpb02/foo/test_foo.py @@ -0,0 +1,11 @@ +from unittest import TestCase + + +class RequiredTest(TestCase): + def test_tests_are_executed(self): + open('test-executed', 'w').close() + + def test_testfiles_exist(self): + open('testfile1.txt').close() + open('testfile2.txt').close() + open('testdir/testfile3.txt').close() diff --git a/tests/tpb02/nested/testfile2.txt b/tests/tpb02/nested/testfile2.txt new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/tpb02/nested/testfile2.txt diff --git a/tests/tpb02/pyproject.toml b/tests/tpb02/pyproject.toml new file mode 100644 index 0000000..a20e0e7 --- /dev/null +++ b/tests/tpb02/pyproject.toml @@ -0,0 +1,22 @@ +[build-system] +requires = ["flit_core >=2,<4"] +build-backend = "flit_core.buildapi" + +[tool.flit.metadata] +module = "foo" +author = "Stefano Rivera" +requires = [ + "tomli", + "importlib-metadata ; python_version < '3.5'" +] + +[tool.flit.metadata.requires-extra] +test = [ + "tox", +] + +[tool.flit.scripts] +foo = "foo:main" + +[tool.flit.external-data] +directory = "data" diff --git a/tests/tpb02/testdir/testfile3.txt b/tests/tpb02/testdir/testfile3.txt new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/tpb02/testdir/testfile3.txt diff --git a/tests/tpb02/testfile1.txt b/tests/tpb02/testfile1.txt new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/tpb02/testfile1.txt diff --git a/tests/tpb03/Makefile b/tests/tpb03/Makefile new file mode 100644 index 0000000..5337be8 --- /dev/null +++ b/tests/tpb03/Makefile @@ -0,0 +1,10 @@ +#!/usr/bin/make -f +include ../common.mk + +check: + # FIXME: This used to be a 2.7 + 3.x test. It may not be useful any more, without 2.x + test -f debian/python3-foo/usr/lib/python3/dist-packages/foo.py + grep -q 'Depends:.*python3-pkg-resources' debian/python3-foo/DEBIAN/control + +clean: + ./debian/rules clean diff --git a/tests/tpb03/debian/changelog b/tests/tpb03/debian/changelog new file mode 100644 index 0000000..322011c --- /dev/null +++ b/tests/tpb03/debian/changelog @@ -0,0 +1,5 @@ +foo (1.2.3) unstable; urgency=low + + * Initial release + + -- Piotr Ozarowski <piotr@debian.org> Tue, 02 Jul 2013 11:02:06 +0200 diff --git a/tests/tpb03/debian/control b/tests/tpb03/debian/control new file mode 100644 index 0000000..e49d924 --- /dev/null +++ b/tests/tpb03/debian/control @@ -0,0 +1,15 @@ +Source: foo +Section: python +Priority: optional +Maintainer: Piotr Ożarowski <piotr@debian.org> +Build-Depends: debhelper-compat (= 12) + , python3-all + , python3-setuptools +# , dh-python +Standards-Version: 3.9.4 + +Package: python3-foo +Architecture: all +Depends: ${python3:Depends}, ${shlibs:Depends}, ${misc:Depends} +Description: package with public CPython modules + example package #1 diff --git a/tests/tpb03/debian/copyright b/tests/tpb03/debian/copyright new file mode 100644 index 0000000..f96adde --- /dev/null +++ b/tests/tpb03/debian/copyright @@ -0,0 +1,2 @@ +The Debian packaging is © 2013, Piotr Ożarowski <piotr@debian.org> and +is licensed under the MIT License. diff --git a/tests/tpb03/debian/rules b/tests/tpb03/debian/rules new file mode 100755 index 0000000..e8953b0 --- /dev/null +++ b/tests/tpb03/debian/rules @@ -0,0 +1,26 @@ +#!/usr/bin/make -f + +export PYBUILD_NAME=foo + +%: + dh $@ + +override_dh_auto_build: + ../../pybuild --build + +override_dh_auto_install: + ../../pybuild --install + +override_dh_auto_test: + ../../pybuild --test + +override_dh_auto_clean: + ../../pybuild --clean + rm -rf .pybuild foo.egg-info + +override_dh_installinit: + DH_VERBOSE=1 ../../dh_python3 + dh_installinit + +override_dh_python3: + # ignore any system dh_python3 diff --git a/tests/tpb03/debian/source/format b/tests/tpb03/debian/source/format new file mode 100644 index 0000000..89ae9db --- /dev/null +++ b/tests/tpb03/debian/source/format @@ -0,0 +1 @@ +3.0 (native) diff --git a/tests/tpb03/foo.py b/tests/tpb03/foo.py new file mode 100644 index 0000000..810c96e --- /dev/null +++ b/tests/tpb03/foo.py @@ -0,0 +1 @@ +"foo" diff --git a/tests/tpb03/setup.cfg b/tests/tpb03/setup.cfg new file mode 100644 index 0000000..e60fea3 --- /dev/null +++ b/tests/tpb03/setup.cfg @@ -0,0 +1,7 @@ +[metadata] +name = foo + +[options] +py_modules = foo +install_requires = + setuptools diff --git a/tests/tpb03/setup.py b/tests/tpb03/setup.py new file mode 100644 index 0000000..6068493 --- /dev/null +++ b/tests/tpb03/setup.py @@ -0,0 +1,3 @@ +from setuptools import setup + +setup() diff --git a/tests/tpb04/Makefile b/tests/tpb04/Makefile new file mode 100644 index 0000000..d1b6324 --- /dev/null +++ b/tests/tpb04/Makefile @@ -0,0 +1,10 @@ +#!/usr/bin/make -f +include ../common.mk + +check: + test -f debian/python3-foo/usr/lib/python3/dist-packages/foo.py + test -f debian/python3-foo-ext/usr/lib/python3/dist-packages/_foo.abi3.so + test -e test-executed + +clean: + ./debian/rules clean diff --git a/tests/tpb04/_foo.c b/tests/tpb04/_foo.c new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/tpb04/_foo.c diff --git a/tests/tpb04/debian/changelog b/tests/tpb04/debian/changelog new file mode 100644 index 0000000..322011c --- /dev/null +++ b/tests/tpb04/debian/changelog @@ -0,0 +1,5 @@ +foo (1.2.3) unstable; urgency=low + + * Initial release + + -- Piotr Ozarowski <piotr@debian.org> Tue, 02 Jul 2013 11:02:06 +0200 diff --git a/tests/tpb04/debian/control b/tests/tpb04/debian/control new file mode 100644 index 0000000..f0643cf --- /dev/null +++ b/tests/tpb04/debian/control @@ -0,0 +1,23 @@ +Source: foo +Section: python +Priority: optional +Maintainer: Piotr Ożarowski <piotr@debian.org> +Build-Depends: debhelper-compat (= 12) + , python3-all-dev + , python3-setuptools + , python3-tomli + , tox +# , dh-python +Standards-Version: 3.9.4 + +Package: python3-foo +Architecture: all +Depends: ${python3:Depends}, ${shlibs:Depends}, ${misc:Depends} +Description: package with public CPython modules + example package #1 + +Package: python3-foo-ext +Architecture: any +Depends: ${python3:Depends}, ${shlibs:Depends}, ${misc:Depends} +Description: package with public CPython extensions + example package #2 diff --git a/tests/tpb04/debian/copyright b/tests/tpb04/debian/copyright new file mode 100644 index 0000000..f96adde --- /dev/null +++ b/tests/tpb04/debian/copyright @@ -0,0 +1,2 @@ +The Debian packaging is © 2013, Piotr Ożarowski <piotr@debian.org> and +is licensed under the MIT License. diff --git a/tests/tpb04/debian/rules b/tests/tpb04/debian/rules new file mode 100755 index 0000000..1a4e00b --- /dev/null +++ b/tests/tpb04/debian/rules @@ -0,0 +1,27 @@ +#!/usr/bin/make -f + +export PYBUILD_NAME=foo +export PYBUILD_EXT_DESTDIR_python3=debian/python3-foo-ext + +%: + dh $@ + +override_dh_auto_build: + ../../pybuild --build + +override_dh_auto_install: + ../../pybuild --install + +override_dh_auto_test: + ../../pybuild --test --test-tox --test-args=-v + +override_dh_auto_clean: + ../../pybuild --clean + rm -rf .pybuild .tox foo.egg-info test-executed + +override_dh_installinit: + DH_VERBOSE=1 ../../dh_python3 + dh_installinit + +override_dh_python3: + # ignore any system dh_python3 diff --git a/tests/tpb04/debian/source/format b/tests/tpb04/debian/source/format new file mode 100644 index 0000000..89ae9db --- /dev/null +++ b/tests/tpb04/debian/source/format @@ -0,0 +1 @@ +3.0 (native) diff --git a/tests/tpb04/foo.py b/tests/tpb04/foo.py new file mode 100644 index 0000000..810c96e --- /dev/null +++ b/tests/tpb04/foo.py @@ -0,0 +1 @@ +"foo" diff --git a/tests/tpb04/setup.cfg b/tests/tpb04/setup.cfg new file mode 100644 index 0000000..3ff1e35 --- /dev/null +++ b/tests/tpb04/setup.cfg @@ -0,0 +1,5 @@ +[metadata] +name = foo + +[options] +py_modules = foo diff --git a/tests/tpb04/setup.py b/tests/tpb04/setup.py new file mode 100644 index 0000000..e922974 --- /dev/null +++ b/tests/tpb04/setup.py @@ -0,0 +1,9 @@ +from setuptools import setup, Extension + +setup(ext_modules=[ + Extension( + '_foo', + ['_foo.c'], + py_limited_api = True, + ) +]) diff --git a/tests/tpb04/test_foo.py b/tests/tpb04/test_foo.py new file mode 100644 index 0000000..bbe6954 --- /dev/null +++ b/tests/tpb04/test_foo.py @@ -0,0 +1,6 @@ +from unittest import TestCase + + +class RequiredTest(TestCase): + def test_tests_are_executed(self): + open('test-executed', 'w').close() diff --git a/tests/tpb04/tox.ini b/tests/tpb04/tox.ini new file mode 100644 index 0000000..08858b5 --- /dev/null +++ b/tests/tpb04/tox.ini @@ -0,0 +1,6 @@ +[tox] +envlist = py39 + +[testenv] +deps = tomli +commands = python -m unittest discover diff --git a/tests/tpb05/LICENSE b/tests/tpb05/LICENSE new file mode 100644 index 0000000..5996299 --- /dev/null +++ b/tests/tpb05/LICENSE @@ -0,0 +1,17 @@ +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. diff --git a/tests/tpb05/Makefile b/tests/tpb05/Makefile new file mode 100644 index 0000000..b9bc2a5 --- /dev/null +++ b/tests/tpb05/Makefile @@ -0,0 +1,19 @@ +#!/usr/bin/make -f +include ../common.mk + +DI=debian/python3-foo/usr/lib/python3/dist-packages/foo-0.1.dist-info + +check: + test -f debian/python3-foo/usr/lib/python3/dist-packages/foo/__init__.py + grep -q ^foo/__init__.py, $(DI)/RECORD + test ! -f $(DI)/direct_url.json + grep -L ^foo-0.1.dist-info/direct_url.json, $(DI)/RECORD | grep -q RECORD + grep -q 'Depends:.*python3-tomli' debian/python3-foo/DEBIAN/control + grep -q 'Depends:.*python3-importlib-metadata \| python3 (>> 3\.5)' debian/python3-foo/DEBIAN/control + grep -L 'Depends:.*tox' debian/python3-foo/DEBIAN/control | grep -q control + find .pybuild -name test-executed | grep -q test-executed + grep -q usr/bin/python3$$ debian/python3-foo/usr/bin/foo + find debian/python3-foo/usr/lib/python3/dist-packages/ -name LICENSE | { ! grep -q LICENSE; } + +clean: + ./debian/rules clean diff --git a/tests/tpb05/debian/changelog b/tests/tpb05/debian/changelog new file mode 100644 index 0000000..322011c --- /dev/null +++ b/tests/tpb05/debian/changelog @@ -0,0 +1,5 @@ +foo (1.2.3) unstable; urgency=low + + * Initial release + + -- Piotr Ozarowski <piotr@debian.org> Tue, 02 Jul 2013 11:02:06 +0200 diff --git a/tests/tpb05/debian/control b/tests/tpb05/debian/control new file mode 100644 index 0000000..76f5dbd --- /dev/null +++ b/tests/tpb05/debian/control @@ -0,0 +1,16 @@ +Source: foo +Section: python +Priority: optional +Maintainer: Piotr Ożarowski <piotr@debian.org> +Build-Depends: debhelper-compat (= 12) + , python3-all + , python3-poetry-core + , python3-pytest + , pybuild-plugin-pyproject +Standards-Version: 3.9.4 + +Package: python3-foo +Architecture: all +Depends: ${python3:Depends}, ${shlibs:Depends}, ${misc:Depends} +Description: package with public CPython modules + example package #1 diff --git a/tests/tpb05/debian/copyright b/tests/tpb05/debian/copyright new file mode 100644 index 0000000..f96adde --- /dev/null +++ b/tests/tpb05/debian/copyright @@ -0,0 +1,2 @@ +The Debian packaging is © 2013, Piotr Ożarowski <piotr@debian.org> and +is licensed under the MIT License. diff --git a/tests/tpb05/debian/rules b/tests/tpb05/debian/rules new file mode 100755 index 0000000..4923534 --- /dev/null +++ b/tests/tpb05/debian/rules @@ -0,0 +1,28 @@ +#!/usr/bin/make -f + +export PYBUILD_NAME=foo + +%: + dh $@ + +override_dh_auto_build: + ../../pybuild --build --verbose + ../../pybuild --build --verbose + +override_dh_auto_install: + ../../pybuild --install + ../../pybuild --install + +override_dh_auto_test: + ../../pybuild --test --test-pytest + +override_dh_auto_clean: + ../../pybuild --clean --verbose + rm -rf .pybuild foo.egg-info + +override_dh_installinit: + DH_VERBOSE=1 ../../dh_python3 + dh_installinit + +override_dh_python3: + # ignore any system dh_python3 diff --git a/tests/tpb05/debian/source/format b/tests/tpb05/debian/source/format new file mode 100644 index 0000000..89ae9db --- /dev/null +++ b/tests/tpb05/debian/source/format @@ -0,0 +1 @@ +3.0 (native) diff --git a/tests/tpb05/foo/__init__.py b/tests/tpb05/foo/__init__.py new file mode 100644 index 0000000..b3b7945 --- /dev/null +++ b/tests/tpb05/foo/__init__.py @@ -0,0 +1,4 @@ +"""An amazing sample package!""" + +def main(): + print("Hello") diff --git a/tests/tpb05/foo/test_foo.py b/tests/tpb05/foo/test_foo.py new file mode 100644 index 0000000..bbe6954 --- /dev/null +++ b/tests/tpb05/foo/test_foo.py @@ -0,0 +1,6 @@ +from unittest import TestCase + + +class RequiredTest(TestCase): + def test_tests_are_executed(self): + open('test-executed', 'w').close() diff --git a/tests/tpb05/pyproject.toml b/tests/tpb05/pyproject.toml new file mode 100644 index 0000000..cb6ebaa --- /dev/null +++ b/tests/tpb05/pyproject.toml @@ -0,0 +1,17 @@ +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry] +name = "foo" +description = "an example module" +version = "0.1" +authors = ["Stefano Rivera <stefanor@debian.org>"] +license = "Expat" + +[tool.poetry.dependencies] +tomli = "^1.0.0" +importlib-metadata = { version="*", python = "<3.5" } + +[tool.poetry.scripts] +foo = 'foo:main' diff --git a/tests/tpb05/setup.py b/tests/tpb05/setup.py new file mode 100644 index 0000000..6593127 --- /dev/null +++ b/tests/tpb05/setup.py @@ -0,0 +1,5 @@ +#!/usr/bin/python3 +import sys + +sys.stderr.write('setup.py was called. Use pep517 instead.\n') +sys.exit(1) diff --git a/tests/tpb06/LICENSE b/tests/tpb06/LICENSE new file mode 100644 index 0000000..5996299 --- /dev/null +++ b/tests/tpb06/LICENSE @@ -0,0 +1,17 @@ +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. diff --git a/tests/tpb06/Makefile b/tests/tpb06/Makefile new file mode 100644 index 0000000..285fe83 --- /dev/null +++ b/tests/tpb06/Makefile @@ -0,0 +1,20 @@ +#!/usr/bin/make -f +include ../common.mk + +DI=debian/python3-foo/usr/lib/python3/dist-packages/foo-0.1.dist-info + +check: + test -f debian/python3-foo/usr/lib/python3/dist-packages/foo/__init__.py + grep -q ^foo/__init__.py, $(DI)/RECORD + test ! -f $(DI)/direct_url.json + grep -L ^foo-0.1.dist-info/direct_url.json, $(DI)/RECORD | grep -q RECORD + grep -q 'Depends:.*python3-tomli' debian/python3-foo/DEBIAN/control + grep -q 'Depends:.*python3-importlib-metadata \| python3 (>> 3\.5)' debian/python3-foo/DEBIAN/control + grep -L 'Depends:.*tox' debian/python3-foo/DEBIAN/control | grep -q control + find .pybuild -name test-executed | grep -q test-executed + grep -q usr/bin/python3$$ debian/python3-foo/usr/bin/foo + find debian/python3-foo/usr/lib/python3/dist-packages/ -name LICENSE | { ! grep -q LICENSE; } + test -f debian/python3-foo/usr/share/man/man1/foo.1.gz + +clean: + ./debian/rules clean diff --git a/tests/tpb06/debian/changelog b/tests/tpb06/debian/changelog new file mode 100644 index 0000000..322011c --- /dev/null +++ b/tests/tpb06/debian/changelog @@ -0,0 +1,5 @@ +foo (1.2.3) unstable; urgency=low + + * Initial release + + -- Piotr Ozarowski <piotr@debian.org> Tue, 02 Jul 2013 11:02:06 +0200 diff --git a/tests/tpb06/debian/control b/tests/tpb06/debian/control new file mode 100644 index 0000000..dec1e1f --- /dev/null +++ b/tests/tpb06/debian/control @@ -0,0 +1,15 @@ +Source: foo +Section: python +Priority: optional +Maintainer: Piotr Ożarowski <piotr@debian.org> +Build-Depends: debhelper-compat (= 12) + , python3-all + , python3-setuptools + , pybuild-plugin-pyproject +Standards-Version: 3.9.4 + +Package: python3-foo +Architecture: any +Depends: ${python3:Depends}, ${shlibs:Depends}, ${misc:Depends} +Description: package with public CPython modules + example package #1 diff --git a/tests/tpb06/debian/copyright b/tests/tpb06/debian/copyright new file mode 100644 index 0000000..f96adde --- /dev/null +++ b/tests/tpb06/debian/copyright @@ -0,0 +1,2 @@ +The Debian packaging is © 2013, Piotr Ożarowski <piotr@debian.org> and +is licensed under the MIT License. diff --git a/tests/tpb06/debian/rules b/tests/tpb06/debian/rules new file mode 100755 index 0000000..db68934 --- /dev/null +++ b/tests/tpb06/debian/rules @@ -0,0 +1,26 @@ +#!/usr/bin/make -f + +export PYBUILD_NAME=foo + +%: + dh $@ + +override_dh_auto_build: + ../../pybuild --build --verbose + +override_dh_auto_install: + ../../pybuild --install + +override_dh_auto_test: + ../../pybuild --test + +override_dh_auto_clean: + ../../pybuild --clean --verbose + rm -rf .pybuild foo.egg-info + +override_dh_installinit: + DH_VERBOSE=1 ../../dh_python3 + dh_installinit + +override_dh_python3: + # ignore any system dh_python3 diff --git a/tests/tpb06/debian/source/format b/tests/tpb06/debian/source/format new file mode 100644 index 0000000..89ae9db --- /dev/null +++ b/tests/tpb06/debian/source/format @@ -0,0 +1 @@ +3.0 (native) diff --git a/tests/tpb06/foo/__init__.py b/tests/tpb06/foo/__init__.py new file mode 100644 index 0000000..b3b7945 --- /dev/null +++ b/tests/tpb06/foo/__init__.py @@ -0,0 +1,4 @@ +"""An amazing sample package!""" + +def main(): + print("Hello") diff --git a/tests/tpb06/foo/ext.c b/tests/tpb06/foo/ext.c new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/tpb06/foo/ext.c diff --git a/tests/tpb06/foo/test_foo.py b/tests/tpb06/foo/test_foo.py new file mode 100644 index 0000000..926af30 --- /dev/null +++ b/tests/tpb06/foo/test_foo.py @@ -0,0 +1,13 @@ +import os +from unittest import TestCase +import subprocess + + +class RequiredTest(TestCase): + def test_tests_are_executed(self): + open('test-executed', 'w').close() + + def test_entry_point_executed(self): + path, _, __ = os.environ['PATH'].partition(":") + assert path.endswith("/scripts") + subprocess.run('foo', check=True) diff --git a/tests/tpb06/man/foo.1 b/tests/tpb06/man/foo.1 new file mode 100644 index 0000000..3bf5dce --- /dev/null +++ b/tests/tpb06/man/foo.1 @@ -0,0 +1,8 @@ +.TH foo 1 "December 5 2022" +.SH NAME +foo \- An example +.SH SYNOPSIS +.B foo +.SH DESCRIPTION +.B foo +says hi. diff --git a/tests/tpb06/pyproject.toml b/tests/tpb06/pyproject.toml new file mode 100644 index 0000000..9787c3b --- /dev/null +++ b/tests/tpb06/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/tests/tpb06/setup.cfg b/tests/tpb06/setup.cfg new file mode 100644 index 0000000..fbd051b --- /dev/null +++ b/tests/tpb06/setup.cfg @@ -0,0 +1,21 @@ +[metadata] +name = foo +version = 0.1 +description = My package description +long_description = My long description +license = Expat + +[options] +zip_safe = False +packages = find: +install_requires = + tomli + importlib-metadata; python_version<'3.5' + +[options.entry_points] +console_scripts = + foo = foo:main + +[options.data_files] +share/man/man1 = + man/foo.1 diff --git a/tests/tpb06/setup.py b/tests/tpb06/setup.py new file mode 100644 index 0000000..de0d34b --- /dev/null +++ b/tests/tpb06/setup.py @@ -0,0 +1,12 @@ +from setuptools import setup, Extension + +setup_args = dict( + ext_modules = [ + Extension( + 'foo.ext', + ['foo/ext.c'], + py_limited_api = True, + ) + ] +) +setup(**setup_args) diff --git a/tests/tpb07/Makefile b/tests/tpb07/Makefile new file mode 100644 index 0000000..5eb4f00 --- /dev/null +++ b/tests/tpb07/Makefile @@ -0,0 +1,11 @@ +#!/usr/bin/make -f +include ../common.mk + +check: + test -f debian/python3-foo/usr/lib/python3/dist-packages/foo/__init__.py + test -f debian/python3-bar/usr/lib/python3/dist-packages/bar/__init__.py + test -f debian/python3-foo/usr/bin/foo + test -f debian/python3-bar/usr/bin/bar + +clean: + ./debian/rules clean diff --git a/tests/tpb07/bar/LICENSE b/tests/tpb07/bar/LICENSE new file mode 100644 index 0000000..5996299 --- /dev/null +++ b/tests/tpb07/bar/LICENSE @@ -0,0 +1,17 @@ +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. diff --git a/tests/tpb07/bar/bar/__init__.py b/tests/tpb07/bar/bar/__init__.py new file mode 100644 index 0000000..717b184 --- /dev/null +++ b/tests/tpb07/bar/bar/__init__.py @@ -0,0 +1,4 @@ +"""An amazing sample package!""" + +def main(): + print("Hello I am bar") diff --git a/tests/tpb07/bar/pyproject.toml b/tests/tpb07/bar/pyproject.toml new file mode 100644 index 0000000..9787c3b --- /dev/null +++ b/tests/tpb07/bar/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/tests/tpb07/bar/setup.cfg b/tests/tpb07/bar/setup.cfg new file mode 100644 index 0000000..296c419 --- /dev/null +++ b/tests/tpb07/bar/setup.cfg @@ -0,0 +1,17 @@ +[metadata] +name = bar +version = 0.1 +description = My package description +long_description = My long description +license = Expat + +[options] +zip_safe = False +packages = find: +install_requires = + tomli + importlib-metadata; python_version<'3.5' + +[options.entry_points] +console_scripts = + bar = bar:main diff --git a/tests/tpb07/debian/changelog b/tests/tpb07/debian/changelog new file mode 100644 index 0000000..fa1a4b0 --- /dev/null +++ b/tests/tpb07/debian/changelog @@ -0,0 +1,5 @@ +foobar (1.2.3) unstable; urgency=low + + * Initial release + + -- Piotr Ozarowski <piotr@debian.org> Tue, 02 Jul 2013 11:02:06 +0200 diff --git a/tests/tpb07/debian/control b/tests/tpb07/debian/control new file mode 100644 index 0000000..6631581 --- /dev/null +++ b/tests/tpb07/debian/control @@ -0,0 +1,21 @@ +Source: foobar +Section: python +Priority: optional +Maintainer: Piotr Ożarowski <piotr@debian.org> +Build-Depends: debhelper-compat (= 12) + , python3-all + , python3-setuptools + , pybuild-plugin-pyproject +Standards-Version: 3.9.4 + +Package: python3-foo +Architecture: all +Depends: ${python3:Depends}, ${misc:Depends} +Description: package with public CPython modules + example package #1 + +Package: python3-bar +Architecture: all +Depends: ${python3:Depends}, ${misc:Depends} +Description: package with public CPython modules + example package #2 diff --git a/tests/tpb07/debian/copyright b/tests/tpb07/debian/copyright new file mode 100644 index 0000000..f96adde --- /dev/null +++ b/tests/tpb07/debian/copyright @@ -0,0 +1,2 @@ +The Debian packaging is © 2013, Piotr Ożarowski <piotr@debian.org> and +is licensed under the MIT License. diff --git a/tests/tpb07/debian/rules b/tests/tpb07/debian/rules new file mode 100755 index 0000000..b93abef --- /dev/null +++ b/tests/tpb07/debian/rules @@ -0,0 +1,30 @@ +#!/usr/bin/make -f + +export PYBUILD_NAME=foo + +%: + dh $@ + +override_dh_auto_build: + ../../pybuild -d foo --build --verbose --name=foo + ../../pybuild -d bar --build --verbose --name=bar + +override_dh_auto_install: + ../../pybuild -d foo --install --name=foo + ../../pybuild -d bar --install --name=bar + +override_dh_auto_test: + ../../pybuild -d foo --test --name=foo + ../../pybuild -d bar --test --name=bar + +override_dh_auto_clean: + ../../pybuild -d foo --clean --verbose --name=foo + ../../pybuild -d bar --clean --verbose --name=bar + rm -rf .pybuild foo/foo.egg-info bar/bar.egg-info + +override_dh_installinit: + DH_VERBOSE=1 ../../dh_python3 + dh_installinit + +override_dh_python3: + # ignore any system dh_python3 diff --git a/tests/tpb07/debian/source/format b/tests/tpb07/debian/source/format new file mode 100644 index 0000000..89ae9db --- /dev/null +++ b/tests/tpb07/debian/source/format @@ -0,0 +1 @@ +3.0 (native) diff --git a/tests/tpb07/foo/LICENSE b/tests/tpb07/foo/LICENSE new file mode 100644 index 0000000..5996299 --- /dev/null +++ b/tests/tpb07/foo/LICENSE @@ -0,0 +1,17 @@ +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. diff --git a/tests/tpb07/foo/foo/__init__.py b/tests/tpb07/foo/foo/__init__.py new file mode 100644 index 0000000..1421a55 --- /dev/null +++ b/tests/tpb07/foo/foo/__init__.py @@ -0,0 +1,4 @@ +"""An amazing sample package!""" + +def main(): + print("Hello I am foo") diff --git a/tests/tpb07/foo/pyproject.toml b/tests/tpb07/foo/pyproject.toml new file mode 100644 index 0000000..9787c3b --- /dev/null +++ b/tests/tpb07/foo/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/tests/tpb07/foo/setup.cfg b/tests/tpb07/foo/setup.cfg new file mode 100644 index 0000000..3447fdc --- /dev/null +++ b/tests/tpb07/foo/setup.cfg @@ -0,0 +1,17 @@ +[metadata] +name = foo +version = 0.1 +description = My package description +long_description = My long description +license = Expat + +[options] +zip_safe = False +packages = find: +install_requires = + tomli + importlib-metadata; python_version<'3.5' + +[options.entry_points] +console_scripts = + foo = foo:main |