diff --git a/.coveragerc b/.coveragerc index ed5b25a..f52803f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -4,6 +4,8 @@ source_pkgs = icrs.releaser # Perhaps this obsoletes the source section in [paths]? relative_files = True branch = true +parallel = true +concurrency = thread [report] # Coverage is run on Linux under cPython 2 and 3, diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8372cd8..f2eca3e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,7 +11,7 @@ jobs: test: strategy: matrix: - python-version: ['3.8', '3.9', '3.10', 'pypy-3.10', '3.11', '3.12'] + python-version: ['3.9', '3.10', 'pypy-3.10', '3.11', '3.12'] runs-on: ubuntu-latest steps: @@ -29,8 +29,10 @@ jobs: python -m pip install -U -e ".[test,docs]" - name: Test run: | - coverage run -m zope.testrunner --test-path=src --auto-color --auto-progress - coverage run -a -m sphinx -b doctest -d docs/_build/doctrees docs docs/_build/doctests + coverage run -m zope.testrunner --package-path src/icrs/releaser icrs.releaser --auto-color --auto-progress + coverage run -m sphinx -b doctest -d docs/_build/doctrees docs docs/_build/doctests + coverage combine + coverage report - name: Submit to Coveralls # This is a container action, which only runs on Linux. uses: AndreMiras/coveralls-python-action@develop diff --git a/CHANGES.rst b/CHANGES.rst index 0e9fedc..9dd613b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,9 +6,13 @@ ================== - Add support for Python 3.11 and 3.12. -- Depend on newer ``zest.releaser >. 9.1.1``. -- Remove dependency on setuptools; now uses the poorly desgined +- Drop support for Python 3.8. The minimum supported version is now 3.9. +- Depend on newer ``zest.releaser >= 9.1.1``. +- Remove dependency on setuptools; now uses the so-called "native" namespace packages. +- Add a new release check that forbids having development dependencies + (e.g., "icrs.releaser >= 3.0.dev0" would be forbidden). This only + works for ``setuptools`` projects that have dependencies listed in setup.py. 1.1.0 (2022-03-03) diff --git a/MANIFEST.in b/MANIFEST.in index 3de5144..b5d4fb3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -18,4 +18,6 @@ recursive-include docs *.py recursive-include docs *.rst recursive-include docs *.css recursive-include docs Makefile + recursive-include src *.zcml +recursive-include src *.txt diff --git a/setup.py b/setup.py index 4d1e3c6..99a27a6 100755 --- a/setup.py +++ b/setup.py @@ -5,10 +5,11 @@ from setuptools import find_namespace_packages -version = '1.1.1.dev0' +version = '1.2.0' entry_points = { 'zest.releaser.prereleaser.before': [ + 'dev_middle = icrs.releaser.devremover:prereleaser_before', # XXX: This only works doing fullrelease 'rm_cflags = icrs.releaser.removecflags:prereleaser_before', ], @@ -16,7 +17,7 @@ 'version_next = icrs.releaser.versionreplacer:prereleaser_middle', # XXX: This only works doing fullrelease 'scm_middle = icrs.releaser.setuptools_scm_versionfixer:prereleaser_middle', - # XXX: Add check for .dev. + ], 'console_scripts': [ 'icrs_release = icrs.releaser.fullrelease:main', @@ -50,7 +51,6 @@ def _read(fname): 'Natural Language :: English', 'Operating System :: OS Independent', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', @@ -64,7 +64,6 @@ def _read(fname): package_dir={'': 'src'}, namespace_packages=['icrs'], install_requires=[ - 'setuptools_scm', 'zest.releaser >= 9.1.1', ], entry_points=entry_points, @@ -80,5 +79,5 @@ def _read(fname): 'zest.releaser[recommended]', ], }, - python_requires=">=3.8", + python_requires=">=3.9", ) diff --git a/src/icrs/releaser/devremover.py b/src/icrs/releaser/devremover.py new file mode 100644 index 0000000..da29614 --- /dev/null +++ b/src/icrs/releaser/devremover.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +""" +See `prereleaser_before`. + +""" + +import re +import sys + +from pathlib import Path + +class DevelopmentDependency(Exception): + """ + Raised when a development dependency is detected. + """ + +# An expression that matches dependency specification lines that have +# ".dev" versions. +# +# TODO: The ``packaging`` library +# (https://packaging.pypa.io/en/stable/index.html) has support to +# actually parse all of these complicated specifiers; the problem is +# extracting which lines need to be parsed like that. We're just +# matching the entire file +_SETUP_PY_DEV_REQUIREMENT_MATCHER = re.compile( + # open single or double quote + br"['\"]" + # one or more alphanumeric characters, underscores, periods + # commas, spaces or square brackets (for extras) + # and zero or more spaces + br"[\w.,\[\] ]+\s*" + # followed by an operator. + # (recall that 'foo < 3, >2, !=2.1' is valid syntax; the clauses can come in + # any order). See https://peps.python.org/pep-0508/ + br"[>== 9.1.1', + ], + entry_points=entry_points, + include_package_data=True, + extras_require={ + 'test': TESTS_REQUIRE, + 'docs': [ + 'Sphinx', + 'furo', + 'sphinxcontrib-programoutput', + ] + TESTS_REQUIRE, + 'recommended': [ + 'zest.releaser[recommended]', + ], + }, + python_requires=">=3.8", +) diff --git a/src/icrs/releaser/tests/test_devremover.py b/src/icrs/releaser/tests/test_devremover.py new file mode 100644 index 0000000..afd92c7 --- /dev/null +++ b/src/icrs/releaser/tests/test_devremover.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- +""" +Tests for devremover.py + +""" +import sys +import tempfile +import unittest +from pathlib import Path +from importlib.resources import files + + +class TestRegex(unittest.TestCase): + + def _callFUT(self, s): + from ..devremover import _SETUP_PY_DEV_REQUIREMENT_MATCHER + s = s.encode('utf-8') if isinstance(s, str) else s + return _SETUP_PY_DEV_REQUIREMENT_MATCHER.search(s) + + def _matches(self, s): + self.assertTrue(self._callFUT(s)) + + def test_simple_matches(self): + m = self._matches + for example in ( + # double quotes + '"icrs.releaser >= 1.0.dev0"', + # single quotes + "'icrs.releaser >= 1.0.dev0'", + # Complex requirement + '"icrs.releaser != 2.0,>=1.0.dev0"', + # From PEP 508 + '\'requests [security,tests] >= 2.8.1.dev0, == 2.8.* ; python_version < "2.7"\'', + ): + with self.subTest(example): + m(example) + + example = f""" + install_requires = [ + {example}, + ] + """ + m(example) + + def _no_match(self, s): + self.assertFalse(self._callFUT(s)) + + def test_simple_non_matches(self): + m = self._no_match + for example in ( + # double quotes + '"icrs.releaser >= 1.0"', + # single quotes + "'icrs.releaser >= 1.0'", + # Complex requirement + '"icrs.releaser != 2.0,>=1.0"', + # From PEP 508 + '\'requests [security,tests] >= 2.8.1, == 2.8.* ; python_version < "2.7"\'', + # A 'dev' substring in various places + "'foo.dev'", + "foo.dev.bar >= 1.0" + ): + with self.subTest(example): + m(example) + + example = f""" + install_requires = [ + {example}, + ] + """ + m(example) + +class TestPrereleaser(unittest.TestCase): + + def setUp(self): + super().setUp() + self._reports = [] + + def _report(self, *args, **_kw): + self._reports.append(args) + + def _callFUT(self, data_dir): + from ..devremover import prereleaser_before as fut + data = { + 'reporoot': data_dir, + 'icrs.releaser:report': self._report, + } + return fut(data) + + def test_no_dev_dep(self): + with tempfile.TemporaryDirectory(prefix='icrs_releaser_') as td: + td = Path(td) + setup = td / 'setup.py' + + example = files('icrs.releaser.tests') / 'example_setup.txt' + setup.write_bytes(example.read_bytes()) + self._callFUT(td) + self.assertTrue(self._reports) + + def test_found_dev_dep(self): + from ..devremover import DevelopmentDependency + with tempfile.TemporaryDirectory(prefix='icrs_releaser_') as td: + td = Path(td) + setup = td / 'setup.py' + + example = files('icrs.releaser.tests') / 'example_setup.txt' + example = example.read_bytes() + placeholder = b'# Placeholder' + dev_dep = b"'icrs.releaser >= 1.0.dev0'," + assert placeholder in example + example = example.replace(placeholder, dev_dep) + assert dev_dep in example + + setup.write_bytes(example) + with self.assertRaisesRegex(DevelopmentDependency, + '.*setup.py had development dependency: ' + + dev_dep.decode('utf-8')[:-2]): + self._callFUT(td) + + def test_no_setup_file(self): + with tempfile.TemporaryDirectory(prefix='icrs_releaser_') as td: + self._callFUT(td) + + self.assertTrue(self._reports) + self.assertEqual(2, len(self._reports)) + self.assertIn('does not exist', ' '.join(str(s) for s in self._reports[1])) + +if __name__ == '__main__': + unittest.main() diff --git a/tox.ini b/tox.ini index 3a4c955..7da78bb 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py38,py39,py310,py311,py312,pypy310,coverage,docs +envlist = py39,py310,py311,py312,pypy310,coverage,docs [testenv] extras = test @@ -17,7 +17,7 @@ commands = coverage report -i coverage html -i coverage xml -i -depends = py38,py39,py310,pypy38,docs +depends = py39,py310,pypy310,docs parallel_show_output = true [testenv:docs]