summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 16:08:06 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 16:08:06 +0000
commitba6d96469df143b52295f8e79da648bf8a597407 (patch)
tree5ea0c3374f5c53209ad02008dcdddfc8ccae92e5
parentInitial commit. (diff)
downloaddh-python-ba6d96469df143b52295f8e79da648bf8a597407.tar.xz
dh-python-ba6d96469df143b52295f8e79da648bf8a597407.zip
Adding upstream version 5.20230130+deb12u1.upstream/5.20230130+deb12u1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
-rw-r--r--.gitlab-ci.yml14
-rw-r--r--Makefile62
-rw-r--r--README.rst175
-rw-r--r--autoscripts/postinst-py3compile6
-rw-r--r--autoscripts/postinst-pycompile3
-rw-r--r--autoscripts/postinst-pypycompile5
-rw-r--r--autoscripts/prerm-py3clean6
-rw-r--r--autoscripts/prerm-pyclean8
-rw-r--r--autoscripts/prerm-pypyclean6
-rw-r--r--dh/pybuild.pm226
-rw-r--r--dh/python3.pm10
-rwxr-xr-xdh_python3292
-rw-r--r--dh_python3.rst238
-rw-r--r--dhpython/__init__.py113
-rwxr-xr-xdhpython/_defaults.py99
-rw-r--r--dhpython/build/__init__.py42
-rw-r--r--dhpython/build/base.py293
-rw-r--r--dhpython/build/plugin_autopkgtest.py38
-rw-r--r--dhpython/build/plugin_cmake.py71
-rw-r--r--dhpython/build/plugin_custom.py48
-rw-r--r--dhpython/build/plugin_distutils.py121
-rw-r--r--dhpython/build/plugin_flit.py170
-rw-r--r--dhpython/build/plugin_pyproject.py201
-rw-r--r--dhpython/debhelper.py327
-rw-r--r--dhpython/depends.py281
-rw-r--r--dhpython/exceptions.py23
-rw-r--r--dhpython/fs.py587
-rw-r--r--dhpython/interpreter.py576
-rw-r--r--dhpython/markers.py70
-rw-r--r--dhpython/option.py30
-rw-r--r--dhpython/pydist.py692
-rw-r--r--dhpython/tools.py340
-rw-r--r--dhpython/version.py457
-rwxr-xr-xpybuild602
-rwxr-xr-xpybuild-autopkgtest68
-rw-r--r--pybuild-autopkgtest.rst109
-rw-r--r--pybuild.rst306
-rw-r--r--pydist/Makefile18
-rw-r--r--pydist/README.PyDist112
-rw-r--r--pydist/cpython3_fallback5173
-rwxr-xr-xpydist/generate_fallback_list.py156
-rw-r--r--tests/Makefile20
-rw-r--r--tests/common.mk19
-rw-r--r--tests/common.py18
-rw-r--r--tests/t301/Makefile15
-rw-r--r--tests/t301/debian/changelog5
-rw-r--r--tests/t301/debian/compat1
-rw-r--r--tests/t301/debian/control18
-rw-r--r--tests/t301/debian/copyright2
-rw-r--r--tests/t301/debian/py3dist-overrides5
-rw-r--r--tests/t301/debian/python3-foo.pydist1
-rwxr-xr-xtests/t301/debian/rules25
-rw-r--r--tests/t301/debian/source/format1
-rw-r--r--tests/t301/lib/foo/__init__.py1
-rw-r--r--tests/t301/lib/foo/bar/__init__.py1
-rw-r--r--tests/t301/lib/foo/baz.py1
l---------tests/t301/lib/foo/jquery.js1
-rw-r--r--tests/t301/setup.py17
-rw-r--r--tests/t302/Makefile18
-rw-r--r--tests/t302/debian/changelog5
-rw-r--r--tests/t302/debian/compat1
-rw-r--r--tests/t302/debian/control18
-rw-r--r--tests/t302/debian/copyright2
-rw-r--r--tests/t302/debian/install2
-rwxr-xr-xtests/t302/debian/rules30
-rw-r--r--tests/t302/debian/source/format1
-rw-r--r--tests/t302/lib/__init__.py0
-rw-r--r--tests/t302/lib/bar.c0
-rw-r--r--tests/t302/lib/foo.py6
-rwxr-xr-xtests/t302/setup.py13
-rw-r--r--tests/t303/Makefile12
-rw-r--r--tests/t303/debian/changelog5
-rw-r--r--tests/t303/debian/compat1
-rw-r--r--tests/t303/debian/control12
-rw-r--r--tests/t303/debian/copyright2
-rwxr-xr-xtests/t303/debian/rules23
-rw-r--r--tests/t303/debian/source/format1
-rw-r--r--tests/t304/Makefile23
-rwxr-xr-xtests/t304/bar.py2
-rwxr-xr-xtests/t304/baz.py2
-rw-r--r--tests/t304/debian/changelog5
-rw-r--r--tests/t304/debian/compat1
-rw-r--r--tests/t304/debian/control19
-rw-r--r--tests/t304/debian/copyright2
-rw-r--r--tests/t304/debian/examples1
-rw-r--r--tests/t304/debian/install7
-rw-r--r--tests/t304/debian/overrides.install2
-rwxr-xr-xtests/t304/debian/rules26
-rw-r--r--tests/t304/debian/source/format1
-rwxr-xr-xtests/t304/foo.py2
-rw-r--r--tests/t304/setup.py0
-rw-r--r--tests/t304/spam.py1
-rw-r--r--tests/t305/Makefile14
-rw-r--r--tests/t305/debian/changelog5
-rw-r--r--tests/t305/debian/compat1
-rw-r--r--tests/t305/debian/control43
-rw-r--r--tests/t305/debian/copyright2
-rw-r--r--tests/t305/debian/foo5a.install1
-rw-r--r--tests/t305/debian/foo5b.install1
-rw-r--r--tests/t305/debian/foo5c.install1
-rw-r--r--tests/t305/debian/foo5d.install1
-rwxr-xr-xtests/t305/debian/rules22
-rw-r--r--tests/t305/debian/source/format1
-rwxr-xr-xtests/t305/foo5a2
-rwxr-xr-xtests/t305/foo5b2
-rwxr-xr-xtests/t305/foo5c2
-rwxr-xr-xtests/t305/foo5d2
-rw-r--r--tests/t306/Makefile17
-rw-r--r--tests/t306/debian/changelog5
-rw-r--r--tests/t306/debian/compat1
-rw-r--r--tests/t306/debian/control13
-rw-r--r--tests/t306/debian/copyright2
-rwxr-xr-xtests/t306/debian/rules14
-rw-r--r--tests/t306/debian/source/format1
-rw-r--r--tests/t306/lib/__init__.py0
-rw-r--r--tests/t306/lib/bar.c0
-rw-r--r--tests/t306/lib/foo.py6
-rwxr-xr-xtests/t306/setup.py12
-rw-r--r--tests/ta01/Makefile39
-rw-r--r--tests/ta01/debian/changelog5
-rw-r--r--tests/ta01/debian/control15
-rwxr-xr-xtests/ta01/debian/rules33
-rw-r--r--tests/ta01/foo/__init__.py1
-rw-r--r--tests/ta01/pyproject.toml3
-rw-r--r--tests/ta01/setup.cfg10
-rw-r--r--tests/ta01/tests/__init__.py0
-rw-r--r--tests/ta01/tests/test_foo.py7
-rw-r--r--tests/ta02/Makefile17
-rw-r--r--tests/ta02/debian/changelog5
-rw-r--r--tests/ta02/debian/control16
-rwxr-xr-xtests/ta02/debian/rules28
-rw-r--r--tests/ta02/foo/__init__.py1
-rw-r--r--tests/ta02/pyproject.toml3
-rw-r--r--tests/ta02/setup.cfg10
-rw-r--r--tests/ta02/tests/__init__.py0
-rw-r--r--tests/ta02/tests/test_foo.py7
-rwxr-xr-xtests/test-package-show-info16
-rw-r--r--tests/test_debhelper.py192
-rw-r--r--tests/test_depends.py726
-rw-r--r--tests/test_fs.py145
-rw-r--r--tests/test_interpreter.py250
-rw-r--r--tests/test_tools.py44
-rw-r--r--tests/tpb01/Makefile9
-rw-r--r--tests/tpb01/debian/changelog5
-rw-r--r--tests/tpb01/debian/control16
-rw-r--r--tests/tpb01/debian/copyright2
-rwxr-xr-xtests/tpb01/debian/rules26
-rw-r--r--tests/tpb01/debian/source/format1
-rw-r--r--tests/tpb01/foo.py1
-rw-r--r--tests/tpb01/setup.cfg9
-rw-r--r--tests/tpb01/setup.py3
-rw-r--r--tests/tpb02/Makefile19
-rw-r--r--tests/tpb02/data/share/man/man1/foo.18
-rw-r--r--tests/tpb02/debian/changelog5
-rw-r--r--tests/tpb02/debian/control16
-rw-r--r--tests/tpb02/debian/copyright2
-rw-r--r--tests/tpb02/debian/pybuild.testfiles3
-rwxr-xr-xtests/tpb02/debian/rules26
-rw-r--r--tests/tpb02/debian/source/format1
-rw-r--r--tests/tpb02/foo/__init__.py6
-rw-r--r--tests/tpb02/foo/test_foo.py11
-rw-r--r--tests/tpb02/nested/testfile2.txt0
-rw-r--r--tests/tpb02/pyproject.toml22
-rw-r--r--tests/tpb02/testdir/testfile3.txt0
-rw-r--r--tests/tpb02/testfile1.txt0
-rw-r--r--tests/tpb03/Makefile10
-rw-r--r--tests/tpb03/debian/changelog5
-rw-r--r--tests/tpb03/debian/control15
-rw-r--r--tests/tpb03/debian/copyright2
-rwxr-xr-xtests/tpb03/debian/rules26
-rw-r--r--tests/tpb03/debian/source/format1
-rw-r--r--tests/tpb03/foo.py1
-rw-r--r--tests/tpb03/setup.cfg7
-rw-r--r--tests/tpb03/setup.py3
-rw-r--r--tests/tpb04/Makefile10
-rw-r--r--tests/tpb04/_foo.c0
-rw-r--r--tests/tpb04/debian/changelog5
-rw-r--r--tests/tpb04/debian/control23
-rw-r--r--tests/tpb04/debian/copyright2
-rwxr-xr-xtests/tpb04/debian/rules27
-rw-r--r--tests/tpb04/debian/source/format1
-rw-r--r--tests/tpb04/foo.py1
-rw-r--r--tests/tpb04/setup.cfg5
-rw-r--r--tests/tpb04/setup.py9
-rw-r--r--tests/tpb04/test_foo.py6
-rw-r--r--tests/tpb04/tox.ini6
-rw-r--r--tests/tpb05/LICENSE17
-rw-r--r--tests/tpb05/Makefile19
-rw-r--r--tests/tpb05/debian/changelog5
-rw-r--r--tests/tpb05/debian/control16
-rw-r--r--tests/tpb05/debian/copyright2
-rwxr-xr-xtests/tpb05/debian/rules28
-rw-r--r--tests/tpb05/debian/source/format1
-rw-r--r--tests/tpb05/foo/__init__.py4
-rw-r--r--tests/tpb05/foo/test_foo.py6
-rw-r--r--tests/tpb05/pyproject.toml17
-rw-r--r--tests/tpb05/setup.py5
-rw-r--r--tests/tpb06/LICENSE17
-rw-r--r--tests/tpb06/Makefile20
-rw-r--r--tests/tpb06/debian/changelog5
-rw-r--r--tests/tpb06/debian/control15
-rw-r--r--tests/tpb06/debian/copyright2
-rwxr-xr-xtests/tpb06/debian/rules26
-rw-r--r--tests/tpb06/debian/source/format1
-rw-r--r--tests/tpb06/foo/__init__.py4
-rw-r--r--tests/tpb06/foo/ext.c0
-rw-r--r--tests/tpb06/foo/test_foo.py13
-rw-r--r--tests/tpb06/man/foo.18
-rw-r--r--tests/tpb06/pyproject.toml3
-rw-r--r--tests/tpb06/setup.cfg21
-rw-r--r--tests/tpb06/setup.py12
-rw-r--r--tests/tpb07/Makefile11
-rw-r--r--tests/tpb07/bar/LICENSE17
-rw-r--r--tests/tpb07/bar/bar/__init__.py4
-rw-r--r--tests/tpb07/bar/pyproject.toml3
-rw-r--r--tests/tpb07/bar/setup.cfg17
-rw-r--r--tests/tpb07/debian/changelog5
-rw-r--r--tests/tpb07/debian/control21
-rw-r--r--tests/tpb07/debian/copyright2
-rwxr-xr-xtests/tpb07/debian/rules30
-rw-r--r--tests/tpb07/debian/source/format1
-rw-r--r--tests/tpb07/foo/LICENSE17
-rw-r--r--tests/tpb07/foo/foo/__init__.py4
-rw-r--r--tests/tpb07/foo/pyproject.toml3
-rw-r--r--tests/tpb07/foo/setup.cfg17
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
diff --git a/pybuild b/pybuild
new file mode 100755
index 0000000..849e097
--- /dev/null
+++ b/pybuild
@@ -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