diff mbox series

python-gpiozero: migrate to setup.cfg

Message ID 20230621134518.304689-1-romain.naour@smile.fr
State Accepted
Headers show
Series python-gpiozero: migrate to setup.cfg | expand

Commit Message

Romain Naour June 21, 2023, 1:45 p.m. UTC
setup.cfg seems to be mandatory since (at least) setuptools 67.8.0
otherwise gpiozero dependencies are not properly detected

File "/builds/buildroot.org/buildroot/test-output/TestPythonPy3Gpiozero/host/lib/python3.11/site-packages/pkg_resources/__init__.py",
line 868, in _resolve_dist
    raise DistributionNotFound(req, requirers)
pkg_resources.DistributionNotFound: The 'colorzero' distribution was not found and is required by the application

See:
http://lists.busybox.net/pipermail/buildroot/2023-June/669207.html

Fixes:
https://gitlab.com/buildroot.org/buildroot/-/jobs/4505977906

Signed-off-by: Romain Naour <romain.naour@smile.fr>
Cc: James Hilliard <james.hilliard1@gmail.com>
---
 .../0001-Migrate-to-setup.cfg.patch           | 855 ++++++++++++++++++
 1 file changed, 855 insertions(+)
 create mode 100644 package/python-gpiozero/0001-Migrate-to-setup.cfg.patch

Comments

Arnout Vandecappelle June 25, 2023, 7:05 p.m. UTC | #1
On 21/06/2023 15:45, Romain Naour wrote:
> setup.cfg seems to be mandatory since (at least) setuptools 67.8.0
> otherwise gpiozero dependencies are not properly detected
> 
> File "/builds/buildroot.org/buildroot/test-output/TestPythonPy3Gpiozero/host/lib/python3.11/site-packages/pkg_resources/__init__.py",
> line 868, in _resolve_dist
>      raise DistributionNotFound(req, requirers)
> pkg_resources.DistributionNotFound: The 'colorzero' distribution was not found and is required by the application
> 
> See:
> http://lists.busybox.net/pipermail/buildroot/2023-June/669207.html
> 
> Fixes:
> https://gitlab.com/buildroot.org/buildroot/-/jobs/4505977906
> 
> Signed-off-by: Romain Naour <romain.naour@smile.fr>
> Cc: James Hilliard <james.hilliard1@gmail.com>

  Applied to master, thanks.

  Regards,
  Arnout

> ---
>   .../0001-Migrate-to-setup.cfg.patch           | 855 ++++++++++++++++++
>   1 file changed, 855 insertions(+)
>   create mode 100644 package/python-gpiozero/0001-Migrate-to-setup.cfg.patch
> 
> diff --git a/package/python-gpiozero/0001-Migrate-to-setup.cfg.patch b/package/python-gpiozero/0001-Migrate-to-setup.cfg.patch
> new file mode 100644
> index 0000000000..090bcbc4a3
> --- /dev/null
> +++ b/package/python-gpiozero/0001-Migrate-to-setup.cfg.patch
> @@ -0,0 +1,855 @@
> +From d56ca0ee4fde7fa4ad82a532f9ea387f9f33d389 Mon Sep 17 00:00:00 2001
> +From: Dave Jones <dave@waveform.org.uk>
> +Date: Tue, 16 Mar 2021 17:52:49 +0000
> +Subject: [PATCH] Migrate to setup.cfg
> +
> +(cherry picked from commit b167bef82c3bf2739707f062b35fcabbb9cc11ad)
> +
> +setup.cfg seems to be mandatory since (at least) setuptools 67.8.0
> +otherwise gpiozero dependencies are not properly detected
> +
> +File "/builds/buildroot.org/buildroot/test-output/TestPythonPy3Gpiozero/host/lib/python3.11/site-packages/pkg_resources/__init__.py",
> +line 868, in _resolve_dist
> +    raise DistributionNotFound(req, requirers)
> +pkg_resources.DistributionNotFound: The 'colorzero' distribution was not found
> +and is required by the application
> +
> +Upstream: https://github.com/gpiozero/gpiozero/commit/b167bef82c3bf2739707f062b35fcabbb9cc11ad
> +
> +See:
> +http://lists.busybox.net/pipermail/buildroot/2023-June/669207.html
> +
> +[Romain: rebase on 1.6.2]
> +Signed-off-by: Romain Naour <romain.naour@smile.fr>
> +---
> + MANIFEST.in    |   4 --
> + Makefile       |  88 +++++---------------------------
> + copyrights     |  56 +++++++++++---------
> + copyrights.cfg |  11 ----
> + coverage.cfg   |  18 -------
> + docs/conf.py   |  99 +++++++++++++++--------------------
> + setup.cfg      |  98 +++++++++++++++++++++++++++++++++++
> + setup.py       | 136 +++++--------------------------------------------
> + 8 files changed, 196 insertions(+), 314 deletions(-)
> + delete mode 100644 MANIFEST.in
> + delete mode 100644 copyrights.cfg
> + delete mode 100644 coverage.cfg
> + create mode 100644 setup.cfg
> +
> +diff --git a/MANIFEST.in b/MANIFEST.in
> +deleted file mode 100644
> +index bb562ea..0000000
> +--- a/MANIFEST.in
> ++++ /dev/null
> +@@ -1,4 +0,0 @@
> +-include README.rst
> +-recursive-include gpiozero/fonts *.txt
> +-recursive-include tests *.py
> +-include LICENSE.rst
> +diff --git a/Makefile b/Makefile
> +index c9ddd90..264a73c 100644
> +--- a/Makefile
> ++++ b/Makefile
> +@@ -4,44 +4,18 @@
> + PYTHON=python
> + PIP=pip
> + PYTEST=py.test
> +-COVERAGE=coverage
> + TWINE=twine
> + PYFLAGS=
> + DEST_DIR=/
> +
> +-# Horrid hack to ensure setuptools is installed in our python environment. This
> +-# is necessary with Python 3.3's venvs which don't install it by default.
> +-ifeq ($(shell python -c "import setuptools" 2>&1),)
> +-SETUPTOOLS:=
> +-else
> +-SETUPTOOLS:=$(shell wget https://bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.py -O - | $(PYTHON))
> +-endif
> +-
> + # Calculate the base names of the distribution, the location of all source,
> + # documentation, packaging, icon, and executable script files
> + NAME:=$(shell $(PYTHON) $(PYFLAGS) setup.py --name)
> + VER:=$(shell $(PYTHON) $(PYFLAGS) setup.py --version)
> +-ifeq ($(shell lsb_release -si),Ubuntu)
> +-DEB_SUFFIX:=ubuntu1
> +-else
> +-DEB_SUFFIX:=
> +-endif
> +-DEB_ARCH:=$(shell dpkg --print-architecture)
> + PYVER:=$(shell $(PYTHON) $(PYFLAGS) -c "import sys; print('py%d.%d' % sys.version_info[:2])")
> + PY_SOURCES:=$(shell \
> + 	$(PYTHON) $(PYFLAGS) setup.py egg_info >/dev/null 2>&1 && \
> + 	grep -v "\.egg-info" $(NAME).egg-info/SOURCES.txt)
> +-DEB_SOURCES:=debian/changelog \
> +-	debian/control \
> +-	debian/copyright \
> +-	debian/rules \
> +-	debian/docs \
> +-	$(wildcard debian/*.init) \
> +-	$(wildcard debian/*.default) \
> +-	$(wildcard debian/*.manpages) \
> +-	$(wildcard debian/*.docs) \
> +-	$(wildcard debian/*.doc-base) \
> +-	$(wildcard debian/*.desktop)
> + DOC_SOURCES:=docs/conf.py \
> + 	$(wildcard docs/*.png) \
> + 	$(wildcard docs/*.svg) \
> +@@ -56,17 +30,6 @@ SUBDIRS:=
> + DIST_WHEEL=dist/$(NAME)-$(VER)-py2.py3-none-any.whl
> + DIST_TAR=dist/$(NAME)-$(VER).tar.gz
> + DIST_ZIP=dist/$(NAME)-$(VER).zip
> +-DIST_DEB=dist/python-$(NAME)_$(VER)$(DEB_SUFFIX)_all.deb \
> +-	dist/python3-$(NAME)_$(VER)$(DEB_SUFFIX)_all.deb \
> +-	dist/python-$(NAME)-doc_$(VER)$(DEB_SUFFIX)_all.deb \
> +-	dist/$(NAME)_$(VER)$(DEB_SUFFIX)_$(DEB_ARCH).build \
> +-	dist/$(NAME)_$(VER)$(DEB_SUFFIX)_$(DEB_ARCH).buildinfo \
> +-	dist/$(NAME)_$(VER)$(DEB_SUFFIX)_$(DEB_ARCH).changes
> +-DIST_DSC=dist/$(NAME)_$(VER)$(DEB_SUFFIX).tar.xz \
> +-	dist/$(NAME)_$(VER)$(DEB_SUFFIX).dsc \
> +-	dist/$(NAME)_$(VER)$(DEB_SUFFIX)_source.build \
> +-	dist/$(NAME)_$(VER)$(DEB_SUFFIX)_source.buildinfo \
> +-	dist/$(NAME)_$(VER)$(DEB_SUFFIX)_source.changes
> +
> +
> + # Default target
> +@@ -76,10 +39,9 @@ all:
> + 	@echo "make test - Run tests"
> + 	@echo "make doc - Generate HTML and PDF documentation"
> + 	@echo "make source - Create source package"
> +-	@echo "make egg - Generate a PyPI egg package"
> ++	@echo "make wheel - Generate a PyPI wheel package"
> + 	@echo "make zip - Generate a source zip package"
> + 	@echo "make tar - Generate a source tar package"
> +-	@echo "make deb - Generate Debian packages"
> + 	@echo "make dist - Generate all packages"
> + 	@echo "make clean - Get rid of all generated files"
> + 	@echo "make release - Create and tag a new release"
> +@@ -102,9 +64,7 @@ zip: $(DIST_ZIP)
> +
> + tar: $(DIST_TAR)
> +
> +-deb: $(DIST_DEB) $(DIST_DSC)
> +-
> +-dist: $(DIST_WHEEL) $(DIST_DEB) $(DIST_DSC) $(DIST_TAR) $(DIST_ZIP)
> ++dist: $(DIST_WHEEL) $(DIST_TAR) $(DIST_ZIP)
> +
> + develop:
> + 	@# These have to be done separately to avoid a cockup...
> +@@ -113,18 +73,17 @@ develop:
> + 	$(PIP) install -e .[doc,test]
> +
> + test:
> +-	$(COVERAGE) run --rcfile coverage.cfg -m $(PYTEST) tests -v -r sx
> +-	$(COVERAGE) report --rcfile coverage.cfg
> ++	$(PYTEST) tests
> +
> + clean:
> +-	dh_clean
> +-	rm -fr dist/ $(NAME).egg-info/ tags
> ++	rm -fr dist/ build/ .pytest_cache/ .mypy_cache/ $(NAME).egg-info/ tags .coverage
> + 	for dir in $(SUBDIRS); do \
> + 		$(MAKE) -C $$dir clean; \
> + 	done
> ++	find $(CURDIR) -name "*.pyc" -delete
> +
> + tags: $(PY_SOURCES)
> +-	ctags -R --exclude="build/*" --exclude="debian/*" --exclude="docs/*" --languages="Python"
> ++	ctags -R --exclude="build/*" --exclude="docs/*" --languages="Python"
> +
> + $(SUBDIRS):
> + 	$(MAKE) -C $@
> +@@ -138,37 +97,14 @@ $(DIST_ZIP): $(PY_SOURCES) $(SUBDIRS)
> + $(DIST_WHEEL): $(PY_SOURCES) $(SUBDIRS)
> + 	$(PYTHON) $(PYFLAGS) setup.py bdist_wheel --universal
> +
> +-$(DIST_DEB): $(PY_SOURCES) $(SUBDIRS) $(DEB_SOURCES) $(DIST_TAR)
> +-	cp $(DIST_TAR) ../$(NAME)_$(VER).orig.tar.gz
> +-	debuild -b
> +-	mkdir -p dist/
> +-	for f in $(DIST_DEB); do cp ../$${f##*/} dist/; done
> +-
> +-$(DIST_DSC): $(PY_SOURCES) $(SUBDIRS) $(DEB_SOURCES) $(DIST_TAR)
> +-	cp $(DIST_TAR) ../$(NAME)_$(VER).orig.tar.gz
> +-	debuild -S
> +-	mkdir -p dist/
> +-	for f in $(DIST_DSC); do cp ../$${f##*/} dist/; done
> +-
> +-copyrights: $(PY_SOURCES) $(DOC_SOURCES)
> +-	./copyrights
> +-
> +-changelog: $(PY_SOURCES) $(DOC_SOURCES) $(DEB_SOURCES)
> ++release:
> + 	$(MAKE) clean
> +-	# ensure there are no current uncommitted changes
> + 	test -z "$(shell git status --porcelain)"
> +-	# update the debian changelog with new release information
> +-	dch --newversion $(VER)$(DEB_SUFFIX)
> +-	# commit the changes and add a new tag
> +-	git commit debian/changelog -m "Updated changelog for release $(VER)"
> +-
> +-release: $(DIST_DEB) $(DIST_DSC) $(DIST_TAR) $(DIST_WHEEL)
> + 	git tag -s v$(VER) -m "Release v$(VER)"
> +-	git push --tags
> +-	# build a source archive and upload to PyPI
> ++	git push origin v$(VER)
> ++
> ++upload: $(DIST_TAR) $(DIST_WHEEL)
> ++	$(TWINE) check $(DIST_TAR) $(DIST_WHEEL)
> + 	$(TWINE) upload $(DIST_TAR) $(DIST_WHEEL)
> +-	# build the deb source archive and upload to Raspbian
> +-	dput raspberrypi dist/$(NAME)_$(VER)$(DEB_SUFFIX)_source.changes
> +-	dput raspberrypi dist/$(NAME)_$(VER)$(DEB_SUFFIX)_$(DEB_ARCH).changes
> +
> +-.PHONY: all install develop test doc source egg wheel zip tar deb dist clean tags release upload $(SUBDIRS)
> ++.PHONY: all install develop test doc source wheel zip tar dist clean tags release upload $(SUBDIRS)
> +diff --git a/copyrights b/copyrights
> +index b709fc8..2f14e78 100755
> +--- a/copyrights
> ++++ b/copyrights
> +@@ -6,10 +6,13 @@ derives the authorship and copyright years information from the git history
> + of the project; hence, this script must be run within a git repository.
> + """
> +
> ++from __future__ import annotations
> ++
> + import os
> + import sys
> + assert sys.version_info >= (3, 6), 'Script requires Python 3.6+'
> + import tempfile
> ++import typing as t
> + from argparse import ArgumentParser, Namespace
> + from configparser import ConfigParser
> + from operator import attrgetter
> +@@ -18,14 +21,13 @@ from datetime import datetime
> + from subprocess import Popen, PIPE, DEVNULL
> + from pathlib import Path
> + from fnmatch import fnmatch
> +-from typing import NamedTuple, Iterator, List, Tuple, Set, Union, Optional
> +
> +
> + SPDX_PREFIX = 'SPDX-License-Identifier:'
> + COPYRIGHT_PREFIX = 'Copyright (c)'
> +
> +
> +-def main(args: List[str] = None):
> ++def main(args: t.List[str] = None):
> +     if args is None:
> +         args = sys.argv[1:]
> +     config = get_config(args)
> +@@ -41,7 +43,7 @@ def main(args: List[str] = None):
> +                     target.write(chunk)
> +
> +
> +-def get_config(args: List[str]) -> Namespace:
> ++def get_config(args: t.List[str]) -> Namespace:
> +     config = ConfigParser(
> +         defaults={
> +             'include': '**/*',
> +@@ -52,10 +54,10 @@ def get_config(args: List[str]) -> Namespace:
> +             'spdx_prefix': SPDX_PREFIX,
> +             'copy_prefix': COPYRIGHT_PREFIX,
> +         },
> +-        delimiters=('=',), default_section='settings',
> ++        delimiters=('=',), default_section='copyrights:settings',
> +         empty_lines_in_values=False, interpolation=None,
> +         converters={'list': lambda s: s.strip().splitlines() })
> +-    config.read('copyrights.cfg')
> ++    config.read('setup.cfg')
> +     sect = config[config.default_section]
> +
> +     parser = ArgumentParser(description=__doc__)
> +@@ -113,10 +115,10 @@ def get_config(args: List[str]) -> Namespace:
> +     return ns
> +
> +
> +-class Copyright(NamedTuple):
> ++class Copyright(t.NamedTuple):
> +     author: str
> +     email:  str
> +-    years:  Set[int]
> ++    years:  t.Set[int]
> +
> +     def __str__(self):
> +         if len(self.years) > 1:
> +@@ -126,8 +128,8 @@ class Copyright(NamedTuple):
> +         return f'{years} {self.author} <{self.email}>'
> +
> +
> +-def get_copyrights(include: Set[str], exclude: Set[str])\
> +-        -> Iterator[Tuple[Path, List[Copyright]]]:
> ++def get_copyrights(include: t.Set[str], exclude: t.Set[str])\
> ++        -> t.Iterator[t.Tuple[Path, t.Container[Copyright]]]:
> +     sorted_blame = sorted(
> +         get_contributions(include, exclude),
> +         key=lambda c: (c.path, c.author, c.email)
> +@@ -147,15 +149,15 @@ def get_copyrights(include: Set[str], exclude: Set[str])\
> +         yield path, copyrights
> +
> +
> +-class Contribution(NamedTuple):
> ++class Contribution(t.NamedTuple):
> +     author: str
> +     email:  str
> +     year:   int
> +     path:   Path
> +
> +
> +-def get_contributions(include: Set[str], exclude: Set[str])\
> +-        -> Iterator[Contribution]:
> ++def get_contributions(include: t.Set[str], exclude: t.Set[str])\
> ++        -> t.Iterator[Contribution]:
> +     for path in get_source_paths(include, exclude):
> +         blame = Popen(
> +             ['git', 'blame', '--line-porcelain', 'HEAD', '--', str(path)],
> +@@ -186,7 +188,8 @@ def get_contributions(include: Set[str], exclude: Set[str])\
> +         assert blame.returncode == 0
> +
> +
> +-def get_source_paths(include: Set[str], exclude: Set[str]) -> Iterator[Path]:
> ++def get_source_paths(include: t.Set[str], exclude: t.Set[str])\
> ++        -> t.Iterator[Path]:
> +     ls_tree = Popen(
> +         ['git', 'ls-tree', '-r', '--name-only', 'HEAD'],
> +         stdout=PIPE, stderr=DEVNULL, universal_newlines=True)
> +@@ -203,9 +206,9 @@ def get_source_paths(include: Set[str], exclude: Set[str]) -> Iterator[Path]:
> +     assert ls_tree.returncode == 0
> +
> +
> +-class License(NamedTuple):
> +-    ident: Optional[str]
> +-    text:  List[str]
> ++class License(t.NamedTuple):
> ++    ident: t.Optional[str]
> ++    text:  t.List[str]
> +
> +
> + def get_license(path: Path, *, spdx_prefix: str = SPDX_PREFIX) -> License:
> +@@ -253,20 +256,26 @@ class CopyWriter:
> +         '.sql': '--',
> +     }
> +
> +-    def __init__(self, license='LICENSE.txt', preamble=(),
> +-                 spdx_prefix=SPDX_PREFIX, copy_prefix=COPYRIGHT_PREFIX):
> ++    def __init__(self, license: Path=Path('LICENSE.txt'),
> ++                 preamble: t.List[str]=None,
> ++                 spdx_prefix: str=SPDX_PREFIX,
> ++                 copy_prefix: str=COPYRIGHT_PREFIX):
> ++        if preamble is None:
> ++            preamble = []
> +         self.license = get_license(license, spdx_prefix=spdx_prefix)
> +         self.preamble = preamble
> +         self.spdx_prefix = spdx_prefix
> +         self.copy_prefix = copy_prefix
> +
> +     @classmethod
> +-    def from_config(cls, config):
> ++    def from_config(cls, config: Namespace) -> CopyWriter:
> +         return cls(
> +             config.license, config.preamble,
> +             config.spdx_prefix, config.copy_prefix)
> +
> +-    def transform(self, source, copyrights, *, comment_prefix=None):
> ++    def transform(self, source: t.TextIO,
> ++                  copyrights: t.List[Copyright], *,
> ++                  comment_prefix: str=None) -> t.Iterator[str]:
> +         if comment_prefix is None:
> +             comment_prefix = self.COMMENTS[Path(source.name).suffix]
> +         license_start = self.license.text[0]
> +@@ -279,7 +288,7 @@ class CopyWriter:
> +                     yield line
> +                     empty = False
> +                 elif linenum < 3 and (
> +-                        'set fileencoding=' in line or '-*- coding:' in line):
> ++                        'fileencoding=' in line or '-*- coding:' in line):
> +                     yield line
> +                     empty = False
> +                 elif line.rstrip() == comment_prefix:
> +@@ -313,7 +322,8 @@ class CopyWriter:
> +             elif state == 'body':
> +                 yield line
> +
> +-    def _generate_header(self, copyrights, comment_prefix, empty):
> ++    def _generate_header(self, copyrights: t.Iterable[Copyright],
> ++                         comment_prefix: str, empty: bool) -> t.Iterator[str]:
> +         if not empty:
> +             yield comment_prefix + '\n'
> +         for line in self.preamble:
> +@@ -356,7 +366,7 @@ class AtomicReplaceFile:
> +         If ``None`` (the default), the temporary file will be opened in binary
> +         mode. Otherwise, this specifies the encoding to use with text mode.
> +     """
> +-    def __init__(self, path: Union[str, Path], encoding: str = None):
> ++    def __init__(self, path: t.Union[str, Path], encoding: str = None):
> +         if isinstance(path, str):
> +             path = Path(path)
> +         self._path = path
> +diff --git a/copyrights.cfg b/copyrights.cfg
> +deleted file mode 100644
> +index de68dbe..0000000
> +--- a/copyrights.cfg
> ++++ /dev/null
> +@@ -1,11 +0,0 @@
> +-[settings]
> +-include=
> +-  **/*.py
> +-  **/*.rst
> +-exclude=
> +-  docs/examples/*.py
> +-  docs/license.rst
> +-license=LICENSE.rst
> +-preamble=
> +-  GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins
> +-strip-preamble=no
> +diff --git a/coverage.cfg b/coverage.cfg
> +deleted file mode 100644
> +index e3f3349..0000000
> +--- a/coverage.cfg
> ++++ /dev/null
> +@@ -1,18 +0,0 @@
> +-[run]
> +-branch = True
> +-include = gpiozero/*
> +-;omit = */bar.py,*/baz.py
> +-
> +-[report]
> +-ignore_errors = True
> +-show_missing = True
> +-exclude_lines =
> +-    pragma: no cover
> +-    assert False
> +-    raise AssertionError
> +-    raise NotImplementedError
> +-    pass
> +-    if __name__ == .__main__.:
> +-
> +-[html]
> +-directory = coverage
> +diff --git a/docs/conf.py b/docs/conf.py
> +index 998ae91..626ae29 100644
> +--- a/docs/conf.py
> ++++ b/docs/conf.py
> +@@ -11,62 +11,34 @@
> +
> + import sys
> + import os
> ++from pathlib import Path
> + from datetime import datetime
> +-sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
> +-on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
> +-import setup as _setup
> +-
> +-# Mock out certain modules while building documentation
> +-class Mock(object):
> +-    __all__ = []
> +-
> +-    def __init__(self, *args, **kw):
> +-        pass
> +-
> +-    def __call__(self, *args, **kw):
> +-        return Mock()
> +-
> +-    def __mul__(self, other):
> +-        return Mock()
> +-
> +-    def __and__(self, other):
> +-        return Mock()
> ++from setuptools.config import read_configuration
> +
> +-    def __bool__(self):
> +-        return False
> +-
> +-    def __nonzero__(self):
> +-        return False
> +-
> +-    @classmethod
> +-    def __getattr__(cls, name):
> +-        if name in ('__file__', '__path__'):
> +-            return '/dev/null'
> +-        else:
> +-            return Mock()
> +-
> +-sys.modules['RPi'] = Mock()
> +-sys.modules['RPi.GPIO'] = sys.modules['RPi'].GPIO
> +-sys.modules['lgpio'] = Mock()
> +-sys.modules['RPIO'] = Mock()
> +-sys.modules['RPIO.PWM'] = sys.modules['RPIO'].PWM
> +-sys.modules['RPIO.Exceptions'] = sys.modules['RPIO'].Exceptions
> +-sys.modules['pigpio'] = Mock()
> +-sys.modules['w1thermsensor'] = Mock()
> +-sys.modules['spidev'] = Mock()
> +-sys.modules['colorzero'] = Mock()
> ++on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
> ++config = read_configuration(str(Path(__file__).parent / '..' / 'setup.cfg'))
> ++info = config['metadata']
> +
> + # -- General configuration ------------------------------------------------
> +
> + extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.intersphinx']
> ++if on_rtd:
> ++    needs_sphinx = '1.4.0'
> ++    extensions.append('sphinx.ext.imgmath')
> ++    imgmath_image_format = 'svg'
> ++    tags.add('rtd')
> ++else:
> ++    extensions.append('sphinx.ext.mathjax')
> ++    mathjax_path = '/usr/share/javascript/mathjax/MathJax.js?config=TeX-AMS_HTML'
> ++
> + templates_path = ['_templates']
> + source_suffix = '.rst'
> + #source_encoding = 'utf-8-sig'
> + master_doc = 'index'
> +-project = 'GPIO Zero'
> +-copyright = '2015-%s %s' % (datetime.now().year, _setup.__author__)
> +-version = _setup.__version__
> +-release = _setup.__version__
> ++project = info['name']
> ++copyright = '2015-{now:%Y} {info[author]}'.format(now=datetime.now(), info=info)
> ++version = info['version']
> ++#release = None
> + #language = None
> + #today_fmt = '%B %d, %Y'
> + exclude_patterns = ['_build']
> +@@ -82,6 +54,15 @@ pygments_style = 'sphinx'
> + # -- Autodoc configuration ------------------------------------------------
> +
> + autodoc_member_order = 'groupwise'
> ++autodoc_mock_imports = [
> ++    'RPi',
> ++    'lgpio',
> ++    'RPIO',
> ++    'pigpio',
> ++    'w1thermsensor',
> ++    'spidev',
> ++    'colorzero',
> ++]
> +
> + # -- Intersphinx configuration --------------------------------------------
> +
> +@@ -94,7 +75,7 @@ intersphinx_mapping = {
> + # -- Options for HTML output ----------------------------------------------
> +
> + html_theme = 'sphinx_rtd_theme'
> +-html_title = '%s %s Documentation' % (project, version)
> ++html_title = '{info[name]} {info[version]} Documentation'.format(info=info)
> + #html_theme_path = []
> + #html_short_title = None
> + #html_logo = None
> +@@ -112,7 +93,7 @@ html_static_path = ['_static']
> + #html_show_copyright = True
> + #html_use_opensearch = ''
> + #html_file_suffix = None
> +-htmlhelp_basename = '%sdoc' % _setup.__project__
> ++htmlhelp_basename = '{info[name]}doc'.format(info=info)
> +
> + # Hack to make wide tables work properly in RTD
> + # See https://github.com/snide/sphinx_rtd_theme/issues/117 for details
> +@@ -131,12 +112,12 @@ latex_elements = {
> +
> + latex_documents = [
> +     (
> +-        'index',                       # source start file
> +-        '%s.tex' % _setup.__project__, # target filename
> +-        '%s Documentation' % project,  # title
> +-        _setup.__author__,             # author
> +-        'manual',                      # documentclass
> +-        True,                          # documents ref'd from toctree only
> ++        'index',            # source start file
> ++        project + '.tex',   # target filename
> ++        html_title,         # title
> ++        info['author'],     # author
> ++        'manual',           # documentclass
> ++        True,               # documents ref'd from toctree only
> +         ),
> + ]
> +
> +@@ -149,11 +130,11 @@ latex_show_urls = 'footnote'
> +
> + # -- Options for epub output ----------------------------------------------
> +
> +-epub_basename = _setup.__project__
> ++epub_basename = project
> + #epub_theme = 'epub'
> + #epub_title = html_title
> +-epub_author = _setup.__author__
> +-epub_identifier = 'https://gpiozero.readthedocs.io/'
> ++epub_author = info['author']
> ++epub_identifier = 'https://{info[name]}.readthedocs.io/'.format(info=info)
> + #epub_tocdepth = 3
> + epub_show_urls = 'no'
> + #epub_use_index = True
> +@@ -161,8 +142,8 @@ epub_show_urls = 'no'
> + # -- Options for manual page output ---------------------------------------
> +
> + man_pages = [
> +-    ('cli_pinout',  'pinout',      'GPIO Zero pinout tool',       [_setup.__author__], 1),
> +-    ('remote_gpio', 'remote-gpio', 'GPIO Zero remote GPIO guide', [_setup.__author__], 7),
> ++    ('cli_pinout',  'pinout',      'GPIO Zero pinout tool',       [info['author']], 1),
> ++    ('remote_gpio', 'remote-gpio', 'GPIO Zero remote GPIO guide', [info['author']], 7),
> + ]
> +
> + man_show_urls = True
> +diff --git a/setup.cfg b/setup.cfg
> +new file mode 100644
> +index 0000000..db0cfa4
> +--- /dev/null
> ++++ b/setup.cfg
> +@@ -0,0 +1,98 @@
> ++[metadata]
> ++name = gpiozero
> ++version = 1.6.2
> ++description = A simple interface to GPIO devices with Raspberry Pi
> ++long_description = file:README.rst
> ++author = Ben Nuttall
> ++author_email = ben@bennuttall.com
> ++url = https://gpiozero.readthedocs.io/
> ++project_urls =
> ++    Documentation = https://gpiozero.readthedocs.io/
> ++    Source Code = https://github.com/gpiozero/gpiozero
> ++    Issue Tracker = https://github.com/gpiozero/gpiozero/issues
> ++keywords = raspberrypi gpio
> ++license = BSD-3-Clause
> ++classifiers =
> ++    Development Status :: 5 - Production/Stable
> ++    Intended Audience :: Education
> ++    Intended Audience :: Developers
> ++    Topic :: Education
> ++    Topic :: System :: Hardware
> ++    License :: OSI Approved :: BSD License
> ++    Programming Language :: Python :: 2
> ++    Programming Language :: Python :: 2.7
> ++    Programming Language :: Python :: 3
> ++    Programming Language :: Python :: 3.5
> ++    Programming Language :: Python :: 3.6
> ++    Programming Language :: Python :: 3.7
> ++    Programming Language :: Python :: 3.8
> ++    Programming Language :: Python :: 3.9
> ++    Programming Language :: Python :: Implementation :: PyPy
> ++
> ++[options]
> ++packages = find:
> ++install_requires =
> ++    colorzero
> ++
> ++[options.package_data]
> ++gpiozero =
> ++    fonts/*.txt
> ++
> ++[options.extras_require]
> ++test =
> ++    pytest
> ++    pytest-cov
> ++doc =
> ++    sphinx
> ++    sphinx-rtd-theme
> ++
> ++[options.entry_points]
> ++console_scripts =
> ++    pinout = gpiozerocli.pinout:main
> ++gpiozero_pin_factories =
> ++    pigpio  = gpiozero.pins.pigpio:PiGPIOFactory
> ++    lgpio   = gpiozero.pins.lgpio:LGPIOFactory
> ++    rpigpio = gpiozero.pins.rpigpio:RPiGPIOFactory
> ++    rpio    = gpiozero.pins.rpio:RPIOFactory
> ++    native  = gpiozero.pins.native:NativeFactory
> ++    mock    = gpiozero.pins.mock:MockFactory
> ++    PiGPIOPin  = gpiozero.pins.pigpio:PiGPIOFactory
> ++    RPiGPIOPin = gpiozero.pins.rpigpio:RPiGPIOFactory
> ++    RPIOPin    = gpiozero.pins.rpio:RPIOFactory
> ++    NativePin  = gpiozero.pins.native:NativeFactory
> ++gpiozero_mock_pin_classes =
> ++    mockpin          = gpiozero.pins.mock:MockPin
> ++    mockpwmpin       = gpiozero.pins.mock:MockPWMPin
> ++    mockchargingpin  = gpiozero.pins.mock:MockChargingPin
> ++    mocktriggerpin   = gpiozero.pins.mock:MockTriggerPin
> ++
> ++[tools:pytest]
> ++addopts = -rsx --cov --tb=short
> ++testpaths = tests
> ++
> ++[coverage:run]
> ++source = gpiozero
> ++branch = true
> ++
> ++[coverage:report]
> ++ignore_errors = true
> ++show_missing = true
> ++exclude_lines =
> ++    pragma: no cover
> ++    assert False
> ++    raise AssertionError
> ++    raise NotImplementedError
> ++    pass
> ++    if __name__ == .__main__.:
> ++
> ++[copyrights:settings]
> ++include =
> ++  **/*.py
> ++  **/*.rst
> ++exclude =
> ++  docs/examples/*.py
> ++  docs/license.rst
> ++license = LICENSE.rst
> ++preamble =
> ++  GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins
> ++strip-preamble = false
> +diff --git a/setup.py b/setup.py
> +index 621398a..fc644b6 100644
> +--- a/setup.py
> ++++ b/setup.py
> +@@ -1,139 +1,29 @@
> +-"A simple interface to GPIO devices with Raspberry Pi."
> ++from __future__ import (
> ++    unicode_literals,
> ++    absolute_import,
> ++)
> ++str = type('')
> +
> + import io
> + import os
> +-import sys
> + import errno
> +-from setuptools import setup, find_packages
> ++from setuptools import setup, config
> +
> +-if sys.version_info[0] == 2:
> +-    if not sys.version_info >= (2, 7):
> +-        raise ValueError('This package requires Python 2.7 or above')
> +-elif sys.version_info[0] == 3:
> +-    if not sys.version_info >= (3, 2):
> +-        raise ValueError('This package requires Python 3.2 or above')
> +-else:
> +-    raise ValueError('Unrecognized major version of Python')
> +-
> +-HERE = os.path.abspath(os.path.dirname(__file__))
> +-
> +-# Workaround <http://www.eby-sarna.com/pipermail/peak/2010-May/003357.html>
> +-try:
> +-    import multiprocessing
> +-except ImportError:
> +-    pass
> +-
> +-__project__      = 'gpiozero'
> +-__version__      = '1.6.2'
> +-__author__       = 'Ben Nuttall'
> +-__author_email__ = 'ben@bennuttall.com'
> +-__url__          = 'https://github.com/gpiozero/gpiozero'
> +-__platforms__    = 'ALL'
> +-
> +-__classifiers__ = [
> +-    "Development Status :: 5 - Production/Stable",
> +-    "Intended Audience :: Education",
> +-    "Intended Audience :: Developers",
> +-    "Topic :: Education",
> +-    "Topic :: System :: Hardware",
> +-    "License :: OSI Approved :: BSD License",
> +-    "Programming Language :: Python :: 2",
> +-    "Programming Language :: Python :: 2.7",
> +-    "Programming Language :: Python :: 3",
> +-    "Programming Language :: Python :: 3.5",
> +-    "Programming Language :: Python :: 3.6",
> +-    "Programming Language :: Python :: 3.7",
> +-    "Programming Language :: Python :: 3.8",
> +-    "Programming Language :: Python :: 3.9",
> +-    "Programming Language :: Python :: Implementation :: PyPy",
> +-]
> +-
> +-__keywords__ = [
> +-    'raspberrypi',
> +-    'gpio',
> +-]
> +-
> +-__requires__ = [
> +-    'colorzero',
> +-]
> +-
> +-__extra_requires__ = {
> +-    'doc':   ['sphinx', 'sphinx_rtd_theme'],
> +-    'test':  ['pytest', 'coverage', 'mock'],
> +-}
> +-
> +-if sys.version_info[:2] == (3, 2):
> +-    # Particular versions are required for Python 3.2 compatibility
> +-    __extra_requires__['doc'].extend([
> +-        'Jinja2<2.7',
> +-        'MarkupSafe<0.16',
> +-        ])
> +-    __extra_requires__['test'][0] = 'pytest<3.0dev'
> +-    __extra_requires__['test'][1] = 'coverage<4.0dev'
> +-elif sys.version_info[:2] == (3, 3):
> +-    __extra_requires__['test'][0] = 'pytest<3.3dev'
> +-elif sys.version_info[:2] == (3, 4):
> +-    __extra_requires__['test'][0] = 'pytest<5.0dev'
> ++cfg = config.read_configuration(
> ++    os.path.join(os.path.dirname(__file__), 'setup.cfg'))
> +
> + try:
> +     # If we're executing on a Raspberry Pi, install all GPIO libraries for
> +     # testing (except RPIO which doesn't work on the multi-core models yet)
> +     with io.open('/proc/device-tree/model', 'r') as f:
> +         if f.read().startswith('Raspberry Pi'):
> +-            __extra_requires__['test'].append('RPi.GPIO')
> +-            __extra_requires__['test'].append('pigpio')
> ++            cfg['options']['extras_require']['test'].append('RPI.GPIO')
> ++            cfg['options']['extras_require']['test'].append('pigpio')
> + except IOError as e:
> +     if e.errno != errno.ENOENT:
> +         raise
> +
> +-__entry_points__ = {
> +-    'gpiozero_pin_factories': [
> +-        'pigpio  = gpiozero.pins.pigpio:PiGPIOFactory',
> +-        'lgpio   = gpiozero.pins.lgpio:LGPIOFactory',
> +-        'rpigpio = gpiozero.pins.rpigpio:RPiGPIOFactory',
> +-        'rpio    = gpiozero.pins.rpio:RPIOFactory',
> +-        'native  = gpiozero.pins.native:NativeFactory',
> +-        'mock    = gpiozero.pins.mock:MockFactory',
> +-        # Backwards compatible names
> +-        'PiGPIOPin  = gpiozero.pins.pigpio:PiGPIOFactory',
> +-        'RPiGPIOPin = gpiozero.pins.rpigpio:RPiGPIOFactory',
> +-        'RPIOPin    = gpiozero.pins.rpio:RPIOFactory',
> +-        'NativePin  = gpiozero.pins.native:NativeFactory',
> +-    ],
> +-    'gpiozero_mock_pin_classes': [
> +-        'mockpin          = gpiozero.pins.mock:MockPin',
> +-        'mockpwmpin       = gpiozero.pins.mock:MockPWMPin',
> +-        'mockchargingpin  = gpiozero.pins.mock:MockChargingPin',
> +-        'mocktriggerpin   = gpiozero.pins.mock:MockTriggerPin',
> +-    ],
> +-    'console_scripts': [
> +-        'pinout = gpiozerocli.pinout:main',
> +-    ]
> +-}
> +-
> +-
> +-def main():
> +-    import io
> +-    with io.open(os.path.join(HERE, 'README.rst'), 'r') as readme:
> +-        setup(
> +-            name                 = __project__,
> +-            version              = __version__,
> +-            description          = __doc__,
> +-            long_description     = readme.read(),
> +-            classifiers          = __classifiers__,
> +-            author               = __author__,
> +-            author_email         = __author_email__,
> +-            url                  = __url__,
> +-            license              = 'BSD-3-Clause',
> +-            keywords             = __keywords__,
> +-            packages             = find_packages(),
> +-            include_package_data = True,
> +-            platforms            = __platforms__,
> +-            install_requires     = __requires__,
> +-            extras_require       = __extra_requires__,
> +-            entry_points         = __entry_points__,
> +-        )
> +-
> ++opts = cfg['metadata']
> ++opts.update(cfg['options'])
> +
> +-if __name__ == '__main__':
> +-    main()
> ++setup(**opts)
> +--
> +2.41.0
> +
diff mbox series

Patch

diff --git a/package/python-gpiozero/0001-Migrate-to-setup.cfg.patch b/package/python-gpiozero/0001-Migrate-to-setup.cfg.patch
new file mode 100644
index 0000000000..090bcbc4a3
--- /dev/null
+++ b/package/python-gpiozero/0001-Migrate-to-setup.cfg.patch
@@ -0,0 +1,855 @@ 
+From d56ca0ee4fde7fa4ad82a532f9ea387f9f33d389 Mon Sep 17 00:00:00 2001
+From: Dave Jones <dave@waveform.org.uk>
+Date: Tue, 16 Mar 2021 17:52:49 +0000
+Subject: [PATCH] Migrate to setup.cfg
+
+(cherry picked from commit b167bef82c3bf2739707f062b35fcabbb9cc11ad)
+
+setup.cfg seems to be mandatory since (at least) setuptools 67.8.0
+otherwise gpiozero dependencies are not properly detected
+
+File "/builds/buildroot.org/buildroot/test-output/TestPythonPy3Gpiozero/host/lib/python3.11/site-packages/pkg_resources/__init__.py",
+line 868, in _resolve_dist
+    raise DistributionNotFound(req, requirers)
+pkg_resources.DistributionNotFound: The 'colorzero' distribution was not found
+and is required by the application
+
+Upstream: https://github.com/gpiozero/gpiozero/commit/b167bef82c3bf2739707f062b35fcabbb9cc11ad
+
+See:
+http://lists.busybox.net/pipermail/buildroot/2023-June/669207.html
+
+[Romain: rebase on 1.6.2]
+Signed-off-by: Romain Naour <romain.naour@smile.fr>
+---
+ MANIFEST.in    |   4 --
+ Makefile       |  88 +++++---------------------------
+ copyrights     |  56 +++++++++++---------
+ copyrights.cfg |  11 ----
+ coverage.cfg   |  18 -------
+ docs/conf.py   |  99 +++++++++++++++--------------------
+ setup.cfg      |  98 +++++++++++++++++++++++++++++++++++
+ setup.py       | 136 +++++--------------------------------------------
+ 8 files changed, 196 insertions(+), 314 deletions(-)
+ delete mode 100644 MANIFEST.in
+ delete mode 100644 copyrights.cfg
+ delete mode 100644 coverage.cfg
+ create mode 100644 setup.cfg
+
+diff --git a/MANIFEST.in b/MANIFEST.in
+deleted file mode 100644
+index bb562ea..0000000
+--- a/MANIFEST.in
++++ /dev/null
+@@ -1,4 +0,0 @@
+-include README.rst
+-recursive-include gpiozero/fonts *.txt
+-recursive-include tests *.py
+-include LICENSE.rst
+diff --git a/Makefile b/Makefile
+index c9ddd90..264a73c 100644
+--- a/Makefile
++++ b/Makefile
+@@ -4,44 +4,18 @@
+ PYTHON=python
+ PIP=pip
+ PYTEST=py.test
+-COVERAGE=coverage
+ TWINE=twine
+ PYFLAGS=
+ DEST_DIR=/
+ 
+-# Horrid hack to ensure setuptools is installed in our python environment. This
+-# is necessary with Python 3.3's venvs which don't install it by default.
+-ifeq ($(shell python -c "import setuptools" 2>&1),)
+-SETUPTOOLS:=
+-else
+-SETUPTOOLS:=$(shell wget https://bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.py -O - | $(PYTHON))
+-endif
+-
+ # Calculate the base names of the distribution, the location of all source,
+ # documentation, packaging, icon, and executable script files
+ NAME:=$(shell $(PYTHON) $(PYFLAGS) setup.py --name)
+ VER:=$(shell $(PYTHON) $(PYFLAGS) setup.py --version)
+-ifeq ($(shell lsb_release -si),Ubuntu)
+-DEB_SUFFIX:=ubuntu1
+-else
+-DEB_SUFFIX:=
+-endif
+-DEB_ARCH:=$(shell dpkg --print-architecture)
+ PYVER:=$(shell $(PYTHON) $(PYFLAGS) -c "import sys; print('py%d.%d' % sys.version_info[:2])")
+ PY_SOURCES:=$(shell \
+ 	$(PYTHON) $(PYFLAGS) setup.py egg_info >/dev/null 2>&1 && \
+ 	grep -v "\.egg-info" $(NAME).egg-info/SOURCES.txt)
+-DEB_SOURCES:=debian/changelog \
+-	debian/control \
+-	debian/copyright \
+-	debian/rules \
+-	debian/docs \
+-	$(wildcard debian/*.init) \
+-	$(wildcard debian/*.default) \
+-	$(wildcard debian/*.manpages) \
+-	$(wildcard debian/*.docs) \
+-	$(wildcard debian/*.doc-base) \
+-	$(wildcard debian/*.desktop)
+ DOC_SOURCES:=docs/conf.py \
+ 	$(wildcard docs/*.png) \
+ 	$(wildcard docs/*.svg) \
+@@ -56,17 +30,6 @@ SUBDIRS:=
+ DIST_WHEEL=dist/$(NAME)-$(VER)-py2.py3-none-any.whl
+ DIST_TAR=dist/$(NAME)-$(VER).tar.gz
+ DIST_ZIP=dist/$(NAME)-$(VER).zip
+-DIST_DEB=dist/python-$(NAME)_$(VER)$(DEB_SUFFIX)_all.deb \
+-	dist/python3-$(NAME)_$(VER)$(DEB_SUFFIX)_all.deb \
+-	dist/python-$(NAME)-doc_$(VER)$(DEB_SUFFIX)_all.deb \
+-	dist/$(NAME)_$(VER)$(DEB_SUFFIX)_$(DEB_ARCH).build \
+-	dist/$(NAME)_$(VER)$(DEB_SUFFIX)_$(DEB_ARCH).buildinfo \
+-	dist/$(NAME)_$(VER)$(DEB_SUFFIX)_$(DEB_ARCH).changes
+-DIST_DSC=dist/$(NAME)_$(VER)$(DEB_SUFFIX).tar.xz \
+-	dist/$(NAME)_$(VER)$(DEB_SUFFIX).dsc \
+-	dist/$(NAME)_$(VER)$(DEB_SUFFIX)_source.build \
+-	dist/$(NAME)_$(VER)$(DEB_SUFFIX)_source.buildinfo \
+-	dist/$(NAME)_$(VER)$(DEB_SUFFIX)_source.changes
+ 
+ 
+ # Default target
+@@ -76,10 +39,9 @@ all:
+ 	@echo "make test - Run tests"
+ 	@echo "make doc - Generate HTML and PDF documentation"
+ 	@echo "make source - Create source package"
+-	@echo "make egg - Generate a PyPI egg package"
++	@echo "make wheel - Generate a PyPI wheel package"
+ 	@echo "make zip - Generate a source zip package"
+ 	@echo "make tar - Generate a source tar package"
+-	@echo "make deb - Generate Debian packages"
+ 	@echo "make dist - Generate all packages"
+ 	@echo "make clean - Get rid of all generated files"
+ 	@echo "make release - Create and tag a new release"
+@@ -102,9 +64,7 @@ zip: $(DIST_ZIP)
+ 
+ tar: $(DIST_TAR)
+ 
+-deb: $(DIST_DEB) $(DIST_DSC)
+-
+-dist: $(DIST_WHEEL) $(DIST_DEB) $(DIST_DSC) $(DIST_TAR) $(DIST_ZIP)
++dist: $(DIST_WHEEL) $(DIST_TAR) $(DIST_ZIP)
+ 
+ develop: 
+ 	@# These have to be done separately to avoid a cockup...
+@@ -113,18 +73,17 @@ develop:
+ 	$(PIP) install -e .[doc,test]
+ 
+ test:
+-	$(COVERAGE) run --rcfile coverage.cfg -m $(PYTEST) tests -v -r sx
+-	$(COVERAGE) report --rcfile coverage.cfg
++	$(PYTEST) tests
+ 
+ clean:
+-	dh_clean
+-	rm -fr dist/ $(NAME).egg-info/ tags
++	rm -fr dist/ build/ .pytest_cache/ .mypy_cache/ $(NAME).egg-info/ tags .coverage
+ 	for dir in $(SUBDIRS); do \
+ 		$(MAKE) -C $$dir clean; \
+ 	done
++	find $(CURDIR) -name "*.pyc" -delete
+ 
+ tags: $(PY_SOURCES)
+-	ctags -R --exclude="build/*" --exclude="debian/*" --exclude="docs/*" --languages="Python"
++	ctags -R --exclude="build/*" --exclude="docs/*" --languages="Python"
+ 
+ $(SUBDIRS):
+ 	$(MAKE) -C $@
+@@ -138,37 +97,14 @@ $(DIST_ZIP): $(PY_SOURCES) $(SUBDIRS)
+ $(DIST_WHEEL): $(PY_SOURCES) $(SUBDIRS)
+ 	$(PYTHON) $(PYFLAGS) setup.py bdist_wheel --universal
+ 
+-$(DIST_DEB): $(PY_SOURCES) $(SUBDIRS) $(DEB_SOURCES) $(DIST_TAR)
+-	cp $(DIST_TAR) ../$(NAME)_$(VER).orig.tar.gz
+-	debuild -b
+-	mkdir -p dist/
+-	for f in $(DIST_DEB); do cp ../$${f##*/} dist/; done
+-
+-$(DIST_DSC): $(PY_SOURCES) $(SUBDIRS) $(DEB_SOURCES) $(DIST_TAR)
+-	cp $(DIST_TAR) ../$(NAME)_$(VER).orig.tar.gz
+-	debuild -S
+-	mkdir -p dist/
+-	for f in $(DIST_DSC); do cp ../$${f##*/} dist/; done
+-
+-copyrights: $(PY_SOURCES) $(DOC_SOURCES)
+-	./copyrights
+-
+-changelog: $(PY_SOURCES) $(DOC_SOURCES) $(DEB_SOURCES)
++release:
+ 	$(MAKE) clean
+-	# ensure there are no current uncommitted changes
+ 	test -z "$(shell git status --porcelain)"
+-	# update the debian changelog with new release information
+-	dch --newversion $(VER)$(DEB_SUFFIX)
+-	# commit the changes and add a new tag
+-	git commit debian/changelog -m "Updated changelog for release $(VER)"
+-
+-release: $(DIST_DEB) $(DIST_DSC) $(DIST_TAR) $(DIST_WHEEL)
+ 	git tag -s v$(VER) -m "Release v$(VER)"
+-	git push --tags
+-	# build a source archive and upload to PyPI
++	git push origin v$(VER)
++
++upload: $(DIST_TAR) $(DIST_WHEEL)
++	$(TWINE) check $(DIST_TAR) $(DIST_WHEEL)
+ 	$(TWINE) upload $(DIST_TAR) $(DIST_WHEEL)
+-	# build the deb source archive and upload to Raspbian
+-	dput raspberrypi dist/$(NAME)_$(VER)$(DEB_SUFFIX)_source.changes
+-	dput raspberrypi dist/$(NAME)_$(VER)$(DEB_SUFFIX)_$(DEB_ARCH).changes
+ 
+-.PHONY: all install develop test doc source egg wheel zip tar deb dist clean tags release upload $(SUBDIRS)
++.PHONY: all install develop test doc source wheel zip tar dist clean tags release upload $(SUBDIRS)
+diff --git a/copyrights b/copyrights
+index b709fc8..2f14e78 100755
+--- a/copyrights
++++ b/copyrights
+@@ -6,10 +6,13 @@ derives the authorship and copyright years information from the git history
+ of the project; hence, this script must be run within a git repository.
+ """
+ 
++from __future__ import annotations
++
+ import os
+ import sys
+ assert sys.version_info >= (3, 6), 'Script requires Python 3.6+'
+ import tempfile
++import typing as t
+ from argparse import ArgumentParser, Namespace
+ from configparser import ConfigParser
+ from operator import attrgetter
+@@ -18,14 +21,13 @@ from datetime import datetime
+ from subprocess import Popen, PIPE, DEVNULL
+ from pathlib import Path
+ from fnmatch import fnmatch
+-from typing import NamedTuple, Iterator, List, Tuple, Set, Union, Optional
+ 
+ 
+ SPDX_PREFIX = 'SPDX-License-Identifier:'
+ COPYRIGHT_PREFIX = 'Copyright (c)'
+ 
+ 
+-def main(args: List[str] = None):
++def main(args: t.List[str] = None):
+     if args is None:
+         args = sys.argv[1:]
+     config = get_config(args)
+@@ -41,7 +43,7 @@ def main(args: List[str] = None):
+                     target.write(chunk)
+ 
+ 
+-def get_config(args: List[str]) -> Namespace:
++def get_config(args: t.List[str]) -> Namespace:
+     config = ConfigParser(
+         defaults={
+             'include': '**/*',
+@@ -52,10 +54,10 @@ def get_config(args: List[str]) -> Namespace:
+             'spdx_prefix': SPDX_PREFIX,
+             'copy_prefix': COPYRIGHT_PREFIX,
+         },
+-        delimiters=('=',), default_section='settings',
++        delimiters=('=',), default_section='copyrights:settings',
+         empty_lines_in_values=False, interpolation=None,
+         converters={'list': lambda s: s.strip().splitlines() })
+-    config.read('copyrights.cfg')
++    config.read('setup.cfg')
+     sect = config[config.default_section]
+ 
+     parser = ArgumentParser(description=__doc__)
+@@ -113,10 +115,10 @@ def get_config(args: List[str]) -> Namespace:
+     return ns
+ 
+ 
+-class Copyright(NamedTuple):
++class Copyright(t.NamedTuple):
+     author: str
+     email:  str
+-    years:  Set[int]
++    years:  t.Set[int]
+ 
+     def __str__(self):
+         if len(self.years) > 1:
+@@ -126,8 +128,8 @@ class Copyright(NamedTuple):
+         return f'{years} {self.author} <{self.email}>'
+ 
+ 
+-def get_copyrights(include: Set[str], exclude: Set[str])\
+-        -> Iterator[Tuple[Path, List[Copyright]]]:
++def get_copyrights(include: t.Set[str], exclude: t.Set[str])\
++        -> t.Iterator[t.Tuple[Path, t.Container[Copyright]]]:
+     sorted_blame = sorted(
+         get_contributions(include, exclude),
+         key=lambda c: (c.path, c.author, c.email)
+@@ -147,15 +149,15 @@ def get_copyrights(include: Set[str], exclude: Set[str])\
+         yield path, copyrights
+ 
+ 
+-class Contribution(NamedTuple):
++class Contribution(t.NamedTuple):
+     author: str
+     email:  str
+     year:   int
+     path:   Path
+ 
+ 
+-def get_contributions(include: Set[str], exclude: Set[str])\
+-        -> Iterator[Contribution]:
++def get_contributions(include: t.Set[str], exclude: t.Set[str])\
++        -> t.Iterator[Contribution]:
+     for path in get_source_paths(include, exclude):
+         blame = Popen(
+             ['git', 'blame', '--line-porcelain', 'HEAD', '--', str(path)],
+@@ -186,7 +188,8 @@ def get_contributions(include: Set[str], exclude: Set[str])\
+         assert blame.returncode == 0
+ 
+ 
+-def get_source_paths(include: Set[str], exclude: Set[str]) -> Iterator[Path]:
++def get_source_paths(include: t.Set[str], exclude: t.Set[str])\
++        -> t.Iterator[Path]:
+     ls_tree = Popen(
+         ['git', 'ls-tree', '-r', '--name-only', 'HEAD'],
+         stdout=PIPE, stderr=DEVNULL, universal_newlines=True)
+@@ -203,9 +206,9 @@ def get_source_paths(include: Set[str], exclude: Set[str]) -> Iterator[Path]:
+     assert ls_tree.returncode == 0
+ 
+ 
+-class License(NamedTuple):
+-    ident: Optional[str]
+-    text:  List[str]
++class License(t.NamedTuple):
++    ident: t.Optional[str]
++    text:  t.List[str]
+ 
+ 
+ def get_license(path: Path, *, spdx_prefix: str = SPDX_PREFIX) -> License:
+@@ -253,20 +256,26 @@ class CopyWriter:
+         '.sql': '--',
+     }
+ 
+-    def __init__(self, license='LICENSE.txt', preamble=(),
+-                 spdx_prefix=SPDX_PREFIX, copy_prefix=COPYRIGHT_PREFIX):
++    def __init__(self, license: Path=Path('LICENSE.txt'),
++                 preamble: t.List[str]=None,
++                 spdx_prefix: str=SPDX_PREFIX,
++                 copy_prefix: str=COPYRIGHT_PREFIX):
++        if preamble is None:
++            preamble = []
+         self.license = get_license(license, spdx_prefix=spdx_prefix)
+         self.preamble = preamble
+         self.spdx_prefix = spdx_prefix
+         self.copy_prefix = copy_prefix
+ 
+     @classmethod
+-    def from_config(cls, config):
++    def from_config(cls, config: Namespace) -> CopyWriter:
+         return cls(
+             config.license, config.preamble,
+             config.spdx_prefix, config.copy_prefix)
+ 
+-    def transform(self, source, copyrights, *, comment_prefix=None):
++    def transform(self, source: t.TextIO,
++                  copyrights: t.List[Copyright], *,
++                  comment_prefix: str=None) -> t.Iterator[str]:
+         if comment_prefix is None:
+             comment_prefix = self.COMMENTS[Path(source.name).suffix]
+         license_start = self.license.text[0]
+@@ -279,7 +288,7 @@ class CopyWriter:
+                     yield line
+                     empty = False
+                 elif linenum < 3 and (
+-                        'set fileencoding=' in line or '-*- coding:' in line):
++                        'fileencoding=' in line or '-*- coding:' in line):
+                     yield line
+                     empty = False
+                 elif line.rstrip() == comment_prefix:
+@@ -313,7 +322,8 @@ class CopyWriter:
+             elif state == 'body':
+                 yield line
+ 
+-    def _generate_header(self, copyrights, comment_prefix, empty):
++    def _generate_header(self, copyrights: t.Iterable[Copyright],
++                         comment_prefix: str, empty: bool) -> t.Iterator[str]:
+         if not empty:
+             yield comment_prefix + '\n'
+         for line in self.preamble:
+@@ -356,7 +366,7 @@ class AtomicReplaceFile:
+         If ``None`` (the default), the temporary file will be opened in binary
+         mode. Otherwise, this specifies the encoding to use with text mode.
+     """
+-    def __init__(self, path: Union[str, Path], encoding: str = None):
++    def __init__(self, path: t.Union[str, Path], encoding: str = None):
+         if isinstance(path, str):
+             path = Path(path)
+         self._path = path
+diff --git a/copyrights.cfg b/copyrights.cfg
+deleted file mode 100644
+index de68dbe..0000000
+--- a/copyrights.cfg
++++ /dev/null
+@@ -1,11 +0,0 @@
+-[settings]
+-include=
+-  **/*.py
+-  **/*.rst
+-exclude=
+-  docs/examples/*.py
+-  docs/license.rst
+-license=LICENSE.rst
+-preamble=
+-  GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins
+-strip-preamble=no
+diff --git a/coverage.cfg b/coverage.cfg
+deleted file mode 100644
+index e3f3349..0000000
+--- a/coverage.cfg
++++ /dev/null
+@@ -1,18 +0,0 @@
+-[run]
+-branch = True
+-include = gpiozero/*
+-;omit = */bar.py,*/baz.py
+-
+-[report]
+-ignore_errors = True
+-show_missing = True
+-exclude_lines =
+-    pragma: no cover
+-    assert False
+-    raise AssertionError
+-    raise NotImplementedError
+-    pass
+-    if __name__ == .__main__.:
+-
+-[html]
+-directory = coverage
+diff --git a/docs/conf.py b/docs/conf.py
+index 998ae91..626ae29 100644
+--- a/docs/conf.py
++++ b/docs/conf.py
+@@ -11,62 +11,34 @@
+ 
+ import sys
+ import os
++from pathlib import Path
+ from datetime import datetime
+-sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
+-on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
+-import setup as _setup
+-
+-# Mock out certain modules while building documentation
+-class Mock(object):
+-    __all__ = []
+-
+-    def __init__(self, *args, **kw):
+-        pass
+-
+-    def __call__(self, *args, **kw):
+-        return Mock()
+-
+-    def __mul__(self, other):
+-        return Mock()
+-
+-    def __and__(self, other):
+-        return Mock()
++from setuptools.config import read_configuration
+ 
+-    def __bool__(self):
+-        return False
+-
+-    def __nonzero__(self):
+-        return False
+-
+-    @classmethod
+-    def __getattr__(cls, name):
+-        if name in ('__file__', '__path__'):
+-            return '/dev/null'
+-        else:
+-            return Mock()
+-
+-sys.modules['RPi'] = Mock()
+-sys.modules['RPi.GPIO'] = sys.modules['RPi'].GPIO
+-sys.modules['lgpio'] = Mock()
+-sys.modules['RPIO'] = Mock()
+-sys.modules['RPIO.PWM'] = sys.modules['RPIO'].PWM
+-sys.modules['RPIO.Exceptions'] = sys.modules['RPIO'].Exceptions
+-sys.modules['pigpio'] = Mock()
+-sys.modules['w1thermsensor'] = Mock()
+-sys.modules['spidev'] = Mock()
+-sys.modules['colorzero'] = Mock()
++on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
++config = read_configuration(str(Path(__file__).parent / '..' / 'setup.cfg'))
++info = config['metadata']
+ 
+ # -- General configuration ------------------------------------------------
+ 
+ extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.intersphinx']
++if on_rtd:
++    needs_sphinx = '1.4.0'
++    extensions.append('sphinx.ext.imgmath')
++    imgmath_image_format = 'svg'
++    tags.add('rtd')
++else:
++    extensions.append('sphinx.ext.mathjax')
++    mathjax_path = '/usr/share/javascript/mathjax/MathJax.js?config=TeX-AMS_HTML'
++
+ templates_path = ['_templates']
+ source_suffix = '.rst'
+ #source_encoding = 'utf-8-sig'
+ master_doc = 'index'
+-project = 'GPIO Zero'
+-copyright = '2015-%s %s' % (datetime.now().year, _setup.__author__)
+-version = _setup.__version__
+-release = _setup.__version__
++project = info['name']
++copyright = '2015-{now:%Y} {info[author]}'.format(now=datetime.now(), info=info)
++version = info['version']
++#release = None
+ #language = None
+ #today_fmt = '%B %d, %Y'
+ exclude_patterns = ['_build']
+@@ -82,6 +54,15 @@ pygments_style = 'sphinx'
+ # -- Autodoc configuration ------------------------------------------------
+ 
+ autodoc_member_order = 'groupwise'
++autodoc_mock_imports = [
++    'RPi',
++    'lgpio',
++    'RPIO',
++    'pigpio',
++    'w1thermsensor',
++    'spidev',
++    'colorzero',
++]
+ 
+ # -- Intersphinx configuration --------------------------------------------
+ 
+@@ -94,7 +75,7 @@ intersphinx_mapping = {
+ # -- Options for HTML output ----------------------------------------------
+ 
+ html_theme = 'sphinx_rtd_theme'
+-html_title = '%s %s Documentation' % (project, version)
++html_title = '{info[name]} {info[version]} Documentation'.format(info=info)
+ #html_theme_path = []
+ #html_short_title = None
+ #html_logo = None
+@@ -112,7 +93,7 @@ html_static_path = ['_static']
+ #html_show_copyright = True
+ #html_use_opensearch = ''
+ #html_file_suffix = None
+-htmlhelp_basename = '%sdoc' % _setup.__project__
++htmlhelp_basename = '{info[name]}doc'.format(info=info)
+ 
+ # Hack to make wide tables work properly in RTD
+ # See https://github.com/snide/sphinx_rtd_theme/issues/117 for details
+@@ -131,12 +112,12 @@ latex_elements = {
+ 
+ latex_documents = [
+     (
+-        'index',                       # source start file
+-        '%s.tex' % _setup.__project__, # target filename
+-        '%s Documentation' % project,  # title
+-        _setup.__author__,             # author
+-        'manual',                      # documentclass
+-        True,                          # documents ref'd from toctree only
++        'index',            # source start file
++        project + '.tex',   # target filename
++        html_title,         # title
++        info['author'],     # author
++        'manual',           # documentclass
++        True,               # documents ref'd from toctree only
+         ),
+ ]
+ 
+@@ -149,11 +130,11 @@ latex_show_urls = 'footnote'
+ 
+ # -- Options for epub output ----------------------------------------------
+ 
+-epub_basename = _setup.__project__
++epub_basename = project
+ #epub_theme = 'epub'
+ #epub_title = html_title
+-epub_author = _setup.__author__
+-epub_identifier = 'https://gpiozero.readthedocs.io/'
++epub_author = info['author']
++epub_identifier = 'https://{info[name]}.readthedocs.io/'.format(info=info)
+ #epub_tocdepth = 3
+ epub_show_urls = 'no'
+ #epub_use_index = True
+@@ -161,8 +142,8 @@ epub_show_urls = 'no'
+ # -- Options for manual page output ---------------------------------------
+ 
+ man_pages = [
+-    ('cli_pinout',  'pinout',      'GPIO Zero pinout tool',       [_setup.__author__], 1),
+-    ('remote_gpio', 'remote-gpio', 'GPIO Zero remote GPIO guide', [_setup.__author__], 7),
++    ('cli_pinout',  'pinout',      'GPIO Zero pinout tool',       [info['author']], 1),
++    ('remote_gpio', 'remote-gpio', 'GPIO Zero remote GPIO guide', [info['author']], 7),
+ ]
+ 
+ man_show_urls = True
+diff --git a/setup.cfg b/setup.cfg
+new file mode 100644
+index 0000000..db0cfa4
+--- /dev/null
++++ b/setup.cfg
+@@ -0,0 +1,98 @@
++[metadata]
++name = gpiozero
++version = 1.6.2
++description = A simple interface to GPIO devices with Raspberry Pi
++long_description = file:README.rst
++author = Ben Nuttall
++author_email = ben@bennuttall.com
++url = https://gpiozero.readthedocs.io/
++project_urls =
++    Documentation = https://gpiozero.readthedocs.io/
++    Source Code = https://github.com/gpiozero/gpiozero
++    Issue Tracker = https://github.com/gpiozero/gpiozero/issues
++keywords = raspberrypi gpio
++license = BSD-3-Clause
++classifiers =
++    Development Status :: 5 - Production/Stable
++    Intended Audience :: Education
++    Intended Audience :: Developers
++    Topic :: Education
++    Topic :: System :: Hardware
++    License :: OSI Approved :: BSD License
++    Programming Language :: Python :: 2
++    Programming Language :: Python :: 2.7
++    Programming Language :: Python :: 3
++    Programming Language :: Python :: 3.5
++    Programming Language :: Python :: 3.6
++    Programming Language :: Python :: 3.7
++    Programming Language :: Python :: 3.8
++    Programming Language :: Python :: 3.9
++    Programming Language :: Python :: Implementation :: PyPy
++
++[options]
++packages = find:
++install_requires =
++    colorzero
++
++[options.package_data]
++gpiozero =
++    fonts/*.txt
++
++[options.extras_require]
++test =
++    pytest
++    pytest-cov
++doc =
++    sphinx
++    sphinx-rtd-theme
++
++[options.entry_points]
++console_scripts =
++    pinout = gpiozerocli.pinout:main
++gpiozero_pin_factories =
++    pigpio  = gpiozero.pins.pigpio:PiGPIOFactory
++    lgpio   = gpiozero.pins.lgpio:LGPIOFactory
++    rpigpio = gpiozero.pins.rpigpio:RPiGPIOFactory
++    rpio    = gpiozero.pins.rpio:RPIOFactory
++    native  = gpiozero.pins.native:NativeFactory
++    mock    = gpiozero.pins.mock:MockFactory
++    PiGPIOPin  = gpiozero.pins.pigpio:PiGPIOFactory
++    RPiGPIOPin = gpiozero.pins.rpigpio:RPiGPIOFactory
++    RPIOPin    = gpiozero.pins.rpio:RPIOFactory
++    NativePin  = gpiozero.pins.native:NativeFactory
++gpiozero_mock_pin_classes =
++    mockpin          = gpiozero.pins.mock:MockPin
++    mockpwmpin       = gpiozero.pins.mock:MockPWMPin
++    mockchargingpin  = gpiozero.pins.mock:MockChargingPin
++    mocktriggerpin   = gpiozero.pins.mock:MockTriggerPin
++
++[tools:pytest]
++addopts = -rsx --cov --tb=short
++testpaths = tests
++
++[coverage:run]
++source = gpiozero
++branch = true
++
++[coverage:report]
++ignore_errors = true
++show_missing = true
++exclude_lines =
++    pragma: no cover
++    assert False
++    raise AssertionError
++    raise NotImplementedError
++    pass
++    if __name__ == .__main__.:
++
++[copyrights:settings]
++include =
++  **/*.py
++  **/*.rst
++exclude =
++  docs/examples/*.py
++  docs/license.rst
++license = LICENSE.rst
++preamble =
++  GPIO Zero: a library for controlling the Raspberry Pi's GPIO pins
++strip-preamble = false
+diff --git a/setup.py b/setup.py
+index 621398a..fc644b6 100644
+--- a/setup.py
++++ b/setup.py
+@@ -1,139 +1,29 @@
+-"A simple interface to GPIO devices with Raspberry Pi."
++from __future__ import (
++    unicode_literals,
++    absolute_import,
++)
++str = type('')
+ 
+ import io
+ import os
+-import sys
+ import errno
+-from setuptools import setup, find_packages
++from setuptools import setup, config
+ 
+-if sys.version_info[0] == 2:
+-    if not sys.version_info >= (2, 7):
+-        raise ValueError('This package requires Python 2.7 or above')
+-elif sys.version_info[0] == 3:
+-    if not sys.version_info >= (3, 2):
+-        raise ValueError('This package requires Python 3.2 or above')
+-else:
+-    raise ValueError('Unrecognized major version of Python')
+-
+-HERE = os.path.abspath(os.path.dirname(__file__))
+-
+-# Workaround <http://www.eby-sarna.com/pipermail/peak/2010-May/003357.html>
+-try:
+-    import multiprocessing
+-except ImportError:
+-    pass
+-
+-__project__      = 'gpiozero'
+-__version__      = '1.6.2'
+-__author__       = 'Ben Nuttall'
+-__author_email__ = 'ben@bennuttall.com'
+-__url__          = 'https://github.com/gpiozero/gpiozero'
+-__platforms__    = 'ALL'
+-
+-__classifiers__ = [
+-    "Development Status :: 5 - Production/Stable",
+-    "Intended Audience :: Education",
+-    "Intended Audience :: Developers",
+-    "Topic :: Education",
+-    "Topic :: System :: Hardware",
+-    "License :: OSI Approved :: BSD License",
+-    "Programming Language :: Python :: 2",
+-    "Programming Language :: Python :: 2.7",
+-    "Programming Language :: Python :: 3",
+-    "Programming Language :: Python :: 3.5",
+-    "Programming Language :: Python :: 3.6",
+-    "Programming Language :: Python :: 3.7",
+-    "Programming Language :: Python :: 3.8",
+-    "Programming Language :: Python :: 3.9",
+-    "Programming Language :: Python :: Implementation :: PyPy",
+-]
+-
+-__keywords__ = [
+-    'raspberrypi',
+-    'gpio',
+-]
+-
+-__requires__ = [
+-    'colorzero',
+-]
+-
+-__extra_requires__ = {
+-    'doc':   ['sphinx', 'sphinx_rtd_theme'],
+-    'test':  ['pytest', 'coverage', 'mock'],
+-}
+-
+-if sys.version_info[:2] == (3, 2):
+-    # Particular versions are required for Python 3.2 compatibility
+-    __extra_requires__['doc'].extend([
+-        'Jinja2<2.7',
+-        'MarkupSafe<0.16',
+-        ])
+-    __extra_requires__['test'][0] = 'pytest<3.0dev'
+-    __extra_requires__['test'][1] = 'coverage<4.0dev'
+-elif sys.version_info[:2] == (3, 3):
+-    __extra_requires__['test'][0] = 'pytest<3.3dev'
+-elif sys.version_info[:2] == (3, 4):
+-    __extra_requires__['test'][0] = 'pytest<5.0dev'
++cfg = config.read_configuration(
++    os.path.join(os.path.dirname(__file__), 'setup.cfg'))
+ 
+ try:
+     # If we're executing on a Raspberry Pi, install all GPIO libraries for
+     # testing (except RPIO which doesn't work on the multi-core models yet)
+     with io.open('/proc/device-tree/model', 'r') as f:
+         if f.read().startswith('Raspberry Pi'):
+-            __extra_requires__['test'].append('RPi.GPIO')
+-            __extra_requires__['test'].append('pigpio')
++            cfg['options']['extras_require']['test'].append('RPI.GPIO')
++            cfg['options']['extras_require']['test'].append('pigpio')
+ except IOError as e:
+     if e.errno != errno.ENOENT:
+         raise
+ 
+-__entry_points__ = {
+-    'gpiozero_pin_factories': [
+-        'pigpio  = gpiozero.pins.pigpio:PiGPIOFactory',
+-        'lgpio   = gpiozero.pins.lgpio:LGPIOFactory',
+-        'rpigpio = gpiozero.pins.rpigpio:RPiGPIOFactory',
+-        'rpio    = gpiozero.pins.rpio:RPIOFactory',
+-        'native  = gpiozero.pins.native:NativeFactory',
+-        'mock    = gpiozero.pins.mock:MockFactory',
+-        # Backwards compatible names
+-        'PiGPIOPin  = gpiozero.pins.pigpio:PiGPIOFactory',
+-        'RPiGPIOPin = gpiozero.pins.rpigpio:RPiGPIOFactory',
+-        'RPIOPin    = gpiozero.pins.rpio:RPIOFactory',
+-        'NativePin  = gpiozero.pins.native:NativeFactory',
+-    ],
+-    'gpiozero_mock_pin_classes': [
+-        'mockpin          = gpiozero.pins.mock:MockPin',
+-        'mockpwmpin       = gpiozero.pins.mock:MockPWMPin',
+-        'mockchargingpin  = gpiozero.pins.mock:MockChargingPin',
+-        'mocktriggerpin   = gpiozero.pins.mock:MockTriggerPin',
+-    ],
+-    'console_scripts': [
+-        'pinout = gpiozerocli.pinout:main',
+-    ]
+-}
+-
+-
+-def main():
+-    import io
+-    with io.open(os.path.join(HERE, 'README.rst'), 'r') as readme:
+-        setup(
+-            name                 = __project__,
+-            version              = __version__,
+-            description          = __doc__,
+-            long_description     = readme.read(),
+-            classifiers          = __classifiers__,
+-            author               = __author__,
+-            author_email         = __author_email__,
+-            url                  = __url__,
+-            license              = 'BSD-3-Clause',
+-            keywords             = __keywords__,
+-            packages             = find_packages(),
+-            include_package_data = True,
+-            platforms            = __platforms__,
+-            install_requires     = __requires__,
+-            extras_require       = __extra_requires__,
+-            entry_points         = __entry_points__,
+-        )
+-
++opts = cfg['metadata']
++opts.update(cfg['options'])
+ 
+-if __name__ == '__main__':
+-    main()
++setup(**opts)
+-- 
+2.41.0
+