diff --git a/CHANGES.rst b/CHANGES.rst index 5d7d03d..0f02107 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,9 @@ Change log 1.1 (unreleased) ---------------- +- Allow specifying a minimum supported Python version other than the previously + hardcoded default of Python 3.8. + 1.0 (2024-10-02) ---------------- diff --git a/docs/narr.rst b/docs/narr.rst index e426997..0bcc435 100644 --- a/docs/narr.rst +++ b/docs/narr.rst @@ -176,6 +176,11 @@ The following options are only needed one time as their values are stored in a final release thus it is not yet generally supported by the zopefoundation packages. +--oldest-python + The oldest version of Python supported by this package. Specified as version + number, e.g. ``3.8``. This setting is optional and defaults to the lowest + Python version generally supported by zopefoundation packages. + --with-docs Enable building the documentation using Sphinx. This will also create a configuration file `.readthedocs.yaml` for integration with diff --git a/src/zope/meta/config_package.py b/src/zope/meta/config_package.py index 607bf5c..1765e41 100644 --- a/src/zope/meta/config_package.py +++ b/src/zope/meta/config_package.py @@ -19,6 +19,8 @@ import jinja2 import tomlkit +from packaging.version import InvalidVersion +from packaging.version import parse as parse_version from .set_branch_protection_rules import set_branch_protection from .shared.call import abort @@ -105,6 +107,11 @@ def handle_command_line_arguments(): default=False, help='Activate support for a future non-final Python version if not' ' already configured in .meta.toml.') + parser.add_argument( + '--oldest-python', + dest='oldest_python', + help='Oldest supported Python version. Defaults to:' + f' {OLDEST_PYTHON_VERSION}.') parser.add_argument( '--with-docs', # people (me) use --with-sphinx and accidentally @@ -197,6 +204,22 @@ def config_type(self): 'Please use `--type` to select it.') return value + @cached_property + def oldest_python(self): + value = (self.args.oldest_python or + self.meta_cfg['python'].get('oldest-python') or + OLDEST_PYTHON_VERSION) + try: + version = parse_version(value) + except InvalidVersion: + raise ValueError(f'Invalid value {value} for oldest Python.') + + if version > parse_version(NEWEST_PYTHON_VERSION): + raise ValueError('Oldest Python version cannot be higher than' + ' newest supported Python') + + return value + @cached_property def config_type_path(self): return pathlib.Path(__file__).parent / self.config_type @@ -348,7 +371,7 @@ def pre_commit_config_yaml(self): "pre-commit-config.yaml.j2", self.path / ".pre-commit-config.yaml", self.config_type, - oldest_python_version=OLDEST_PYTHON_VERSION.replace(".", ""), + oldest_python_version=self.oldest_python.replace(".", ""), teyit_exclude=teyit_exclude, ) @@ -391,7 +414,7 @@ def manylinux_sh(self): with_future_python=self.with_future_python, future_python_shortversion=FUTURE_PYTHON_SHORTVERSION, supported_python_versions=supported_python_versions( - short_version=True), + self.oldest_python, short_version=True), stop_at=stop_at, ) (self.path / '.manylinux-install.sh').chmod(0o755) @@ -477,7 +500,7 @@ def tox(self): setuptools_version_spec=SETUPTOOLS_VERSION_SPEC, future_python_shortversion=FUTURE_PYTHON_SHORTVERSION, supported_python_versions=supported_python_versions( - short_version=True), + self.oldest_python, short_version=True), ) def tests_yml(self): @@ -496,8 +519,10 @@ def tests_yml(self): require_cffi = self.meta_cfg.get( 'c-code', {}).get('require-cffi', False) py_version_matrix = [ - x for x in zip(supported_python_versions(short_version=False), - supported_python_versions(short_version=True))] + x for x in zip(supported_python_versions(self.oldest_python, + short_version=False), + supported_python_versions(self.oldest_python, + short_version=True))] self.copy_with_meta( 'tests.yml.j2', workflows / 'tests.yml', diff --git a/src/zope/meta/set_branch_protection_rules.py b/src/zope/meta/set_branch_protection_rules.py index fdf2e76..a77beba 100644 --- a/src/zope/meta/set_branch_protection_rules.py +++ b/src/zope/meta/set_branch_protection_rules.py @@ -23,7 +23,6 @@ BASE_URL = f'https://raw.githubusercontent.com/{ORG}' -OLDEST_PYTHON = f'py{OLDEST_PYTHON_VERSION.replace(".", "")}' NEWEST_PYTHON = f'py{NEWEST_PYTHON_VERSION.replace(".", "")}' DEFAULT_BRANCH = 'master' @@ -71,6 +70,9 @@ def set_branch_protection( template = meta_toml['meta']['template'] with_docs = meta_toml['python'].get('with-docs', False) with_pypy = meta_toml['python']['with-pypy'] + oldest_python_version = meta_toml['python'].get('oldest-python', + OLDEST_PYTHON_VERSION) + oldest_python = oldest_python_version.replace('.', '') with_windows = meta_toml['python']['with-windows'] with_macos = meta_toml['python']['with-macos'] required = ['linting'] @@ -79,9 +81,9 @@ def set_branch_protection( f'manylinux ({MANYLINUX_PYTHON_VERSION}, {MANYLINUX_AARCH64})', f'manylinux ({MANYLINUX_PYTHON_VERSION}, {MANYLINUX_I686})', f'manylinux ({MANYLINUX_PYTHON_VERSION}, {MANYLINUX_X86_64})', - f'test ({OLDEST_PYTHON_VERSION}, macos-latest)', + f'test ({oldest_python_version}, macos-latest)', f'test ({NEWEST_PYTHON_VERSION}, macos-latest)', - f'test ({OLDEST_PYTHON_VERSION}, ubuntu-latest)', + f'test ({oldest_python_version}, ubuntu-latest)', f'test ({NEWEST_PYTHON_VERSION}, ubuntu-latest)', 'coveralls_finish', ]) @@ -93,23 +95,23 @@ def set_branch_protection( required.append(f'test (pypy-{PYPY_VERSION}, windows-latest)') if with_windows: required.extend([ - f'test ({OLDEST_PYTHON_VERSION}, windows-latest)', + f'test ({oldest_python_version}, windows-latest)', f'test ({NEWEST_PYTHON_VERSION}, windows-latest)', ]) elif with_windows or with_macos: required.extend([ 'ubuntu-coverage', - f'ubuntu-{OLDEST_PYTHON}', + f'ubuntu-{oldest_python}', f'ubuntu-{NEWEST_PYTHON}', ]) if with_windows: required.extend([ - f'windows-{OLDEST_PYTHON}', + f'windows-{oldest_python}', f'windows-{NEWEST_PYTHON}', ]) if with_macos: required.extend([ - f'macos-{OLDEST_PYTHON}', + f'macos-{oldest_python}', f'macos-{NEWEST_PYTHON}', ]) if with_pypy: @@ -120,7 +122,7 @@ def set_branch_protection( if with_docs: required.append('ubuntu-docs') else: # default for most packages - required.extend([OLDEST_PYTHON, NEWEST_PYTHON]) + required.extend([oldest_python, NEWEST_PYTHON]) if template != 'toolkit': required.append('coverage') if with_docs: diff --git a/src/zope/meta/shared/packages.py b/src/zope/meta/shared/packages.py index 7b072d4..de5ef3c 100644 --- a/src/zope/meta/shared/packages.py +++ b/src/zope/meta/shared/packages.py @@ -145,7 +145,8 @@ def parse_additional_config(cfg): return data -def supported_python_versions(short_version=False): +def supported_python_versions(oldest_version=OLDEST_PYTHON_VERSION, + short_version=False): """Create a list containing all supported Python versions Uses the configured oldest and newest Python versions to compute a list @@ -153,12 +154,14 @@ def supported_python_versions(short_version=False): the templates. Kwargs: + oldest_version (str): + The oldest supported Python version, e.g. '3.8'. short_version (bool): - Return short versions like "313" instead of "3.13" + Return short versions like "313" instead of "3.13". Default False. """ minor_versions = [] - oldest_python = parse_version(OLDEST_PYTHON_VERSION) + oldest_python = parse_version(oldest_version) newest_python = parse_version(NEWEST_PYTHON_VERSION) for minor in range(oldest_python.minor, newest_python.minor + 1): minor_versions.append(minor) diff --git a/src/zope/meta/update_python_support.py b/src/zope/meta/update_python_support.py index 4aa86ab..d3be630 100644 --- a/src/zope/meta/update_python_support.py +++ b/src/zope/meta/update_python_support.py @@ -84,14 +84,20 @@ def main(): with open('.meta.toml', 'rb') as meta_f: meta_toml = collections.defaultdict(dict, **tomlkit.load(meta_f)) config_type = meta_toml['meta']['template'] + oldest_python_version = meta_toml['python'].get('oldest-python', + OLDEST_PYTHON_VERSION) branch_name = get_branch_name(args.branch_name, config_type) updating = git_branch(branch_name) current_python_versions = get_tox_ini_python_versions('tox.ini') - no_longer_supported = (set(current_python_versions) - - set(supported_python_versions())) - not_yet_supported = (set(supported_python_versions()) - - set(current_python_versions)) + no_longer_supported = ( + set(current_python_versions) - + set(supported_python_versions(oldest_python_version)) + ) + not_yet_supported = ( + set(supported_python_versions(oldest_python_version)) - + set(current_python_versions) + ) non_interactive_params = [] python_versions_args = [] @@ -107,21 +113,23 @@ def main(): sys.exit(0) if no_longer_supported: - for version in sorted(list(no_longer_supported)): - call(bin_dir / 'addchangelogentry', - f'Drop support for Python {version}.', - *non_interactive_params) + version_spec = ', '.join(sorted(no_longer_supported)) + call(bin_dir / 'addchangelogentry', + f'Drop support for Python {version_spec}.', + *non_interactive_params) python_versions_args.append( '--drop=' + ','.join(no_longer_supported)) if not_yet_supported: - for version in sorted(list(not_yet_supported)): - call( - bin_dir / 'addchangelogentry', - f'Add support for Python {version}.', - *non_interactive_params) - python_versions_args = ['--add=' + - ','.join(supported_python_versions())] + version_spec = ', '.join(sorted(not_yet_supported)) + call( + bin_dir / 'addchangelogentry', + f'Add support for Python {version_spec}.', + *non_interactive_params) + python_versions_args = [ + '--add=' + + ','.join(supported_python_versions(oldest_python_version)) + ] if no_longer_supported or not_yet_supported: call(bin_dir / 'check-python-versions', '--only=setup.py', @@ -130,8 +138,7 @@ def main(): call(os.environ['EDITOR'], '.meta.toml') config_package_args = [ - sys.executable, - 'config-package.py', + bin_dir / 'config-package', path, f'--branch={branch_name}', '--no-push', @@ -140,7 +147,7 @@ def main(): config_package_args.append('--no-commit') call(*config_package_args, cwd=cwd_str) src = path.resolve() / 'src' - py_ver_plus = f'--py{OLDEST_PYTHON_VERSION.replace(".", "")}-plus' + py_ver_plus = f'--py{oldest_python_version.replace(".", "")}-plus' call('find', src, '-name', '*.py', '-exec', bin_dir / 'pyupgrade', '--py3-plus', py_ver_plus, '{}', ';') call(bin_dir / 'pyupgrade',