diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..c5508b9f
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,39 @@
+# Generated from:
+# https://github.com/zopefoundation/meta/tree/master/config/pure-python
+#
+# EditorConfig Configuration file, for more details see:
+# http://EditorConfig.org
+# EditorConfig is a convention description, that could be interpreted
+# by multiple editors to enforce common coding conventions for specific
+# file types
+
+# top-most EditorConfig file:
+# Will ignore other EditorConfig files in Home directory or upper tree level.
+root = true
+
+
+[*] # For All Files
+# Unix-style newlines with a newline ending every file
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+# Set default charset
+charset = utf-8
+# Indent style default
+indent_style = space
+# Max Line Length - a hard line wrap, should be disabled
+max_line_length = off
+
+[*.{py,cfg,ini}]
+# 4 space indentation
+indent_size = 4
+
+[*.{yml,zpt,pt,dtml,zcml}]
+# 2 space indentation
+indent_size = 2
+
+[{Makefile,.gitmodules}]
+# Tab indentation (no size specified, but view as 4 spaces)
+indent_style = tab
+indent_size = unset
+tab_width = unset
diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml
index 8f7c0da7..0f06e163 100644
--- a/.github/workflows/pre-commit.yml
+++ b/.github/workflows/pre-commit.yml
@@ -1,3 +1,5 @@
+# Generated from:
+# https://github.com/zopefoundation/meta/tree/master/config/pure-python
name: pre-commit
on:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 00000000..f8f824e9
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,67 @@
+# Generated from:
+# https://github.com/zopefoundation/meta/tree/master/config/pure-python
+name: tests
+
+on:
+ push:
+ pull_request:
+ schedule:
+ - cron: '0 12 * * 0' # run once a week on Sunday
+ # Allow to run this workflow manually from the Actions tab
+ workflow_dispatch:
+
+jobs:
+ build:
+ strategy:
+ # We want to see all failures:
+ fail-fast: false
+ matrix:
+ os:
+ - ["ubuntu", "ubuntu-latest"]
+ config:
+ # [Python version, tox env]
+ - ["3.11", "release-check"]
+ - ["3.8", "py38"]
+ - ["3.9", "py39"]
+ - ["3.10", "py310"]
+ - ["3.11", "py311"]
+ - ["3.12", "py312"]
+ - ["3.13", "py313"]
+ - ["3.11", "docs"]
+ - ["3.11", "coverage"]
+
+ runs-on: ${{ matrix.os[1] }}
+ if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
+ name: ${{ matrix.config[1] }}
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ matrix.config[0] }}
+ allow-prereleases: true
+ - name: Pip cache
+ uses: actions/cache@v4
+ with:
+ path: ~/.cache/pip
+ key: ${{ runner.os }}-pip-${{ matrix.config[0] }}-${{ hashFiles('setup.*', 'tox.ini') }}
+ restore-keys: |
+ ${{ runner.os }}-pip-${{ matrix.config[0] }}-
+ ${{ runner.os }}-pip-
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install tox
+ - name: Test
+ if: ${{ !startsWith(runner.os, 'Mac') }}
+ run: tox -e ${{ matrix.config[1] }}
+ - name: Test (macOS)
+ if: ${{ startsWith(runner.os, 'Mac') }}
+ run: tox -e ${{ matrix.config[1] }}-universal2
+ - name: Coverage
+ if: matrix.config[1] == 'coverage'
+ run: |
+ pip install coveralls
+ coveralls --service=github
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitignore b/.gitignore
index 13182326..1f321f52 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,8 +1,32 @@
+# Generated from:
+# https://github.com/zopefoundation/meta/tree/master/config/pure-python
+*.dll
+*.egg-info/
+*.profraw
+*.pyc
+*.pyo
+*.so
+.coverage
+.coverage.*
+.eggs/
+.installed.cfg
+.mr.developer.cfg
+.tox/
+.vscode/
__pycache__/
-config/bin/
-config/lib/
-config/lib64/
-config/pyvenv.cfg
-
-# lib64 is a symlink on Ubuntu, so we need it without the trailing slash
-config/lib64
+bin/
+build/
+coverage.xml
+develop-eggs/
+develop/
+dist/
+docs/_build
+eggs/
+etc/
+lib/
+lib64
+log/
+parts/
+pyvenv.cfg
+testing.log
+var/
diff --git a/.isort.cfg b/.isort.cfg
deleted file mode 100644
index 63626095..00000000
--- a/.isort.cfg
+++ /dev/null
@@ -1,8 +0,0 @@
-[isort]
-lines_between_sections = 0
-lines_after_imports = 2
-no_sections = True
-from_first = True
-lines_between_types = 0
-force_single_line = True
-case_sensitive = True
diff --git a/.meta.toml b/.meta.toml
new file mode 100644
index 00000000..115dcce9
--- /dev/null
+++ b/.meta.toml
@@ -0,0 +1,37 @@
+# Generated from:
+# https://github.com/zopefoundation/meta/tree/master/config/pure-python
+[meta]
+template = "pure-python"
+commit-id = "35d10997"
+
+[python]
+with-windows = false
+with-pypy = false
+with-future-python = false
+with-docs = true
+with-sphinx-doctests = false
+with-macos = false
+
+[tox]
+use-flake8 = true
+
+[coverage]
+fail-under = 20
+
+[coverage-run]
+source = "zope.meta"
+
+[manifest]
+additional-rules = [
+ "include *.yaml",
+ "recursive-include docs *.bat",
+ "recursive-include src *.j2",
+ "recursive-include src *.md",
+ "recursive-include src *.sh",
+ "recursive-include src *.txt",
+ ]
+
+[check-manifest]
+additional-ignores = [
+ "docs/_build/html/_static/scripts/*",
+ ]
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 4960c26b..7ab398cc 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,48 +1,28 @@
----
+# Generated from:
+# https://github.com/zopefoundation/meta/tree/master/config/pure-python
+minimum_pre_commit_version: '3.6'
repos:
- - repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.6.0
- hooks:
- - id: trailing-whitespace
- - id: end-of-file-fixer
- - id: fix-encoding-pragma
- args: [--remove]
- - id: check-yaml
- - id: debug-statements
- language_version: python3
- - repo: https://github.com/pre-commit/mirrors-autopep8
- rev: v2.0.4
- hooks:
- - id: autopep8
- - repo: https://github.com/isidentical/teyit
- rev: 0.4.3
- hooks:
- - id: teyit
- - repo: https://github.com/PyCQA/flake8
- rev: 7.1.1
- hooks:
- - id: flake8
- language_version: python3
- additional_dependencies: [flake8-typing-imports==1.15.0]
- - repo: https://github.com/timothycrosley/isort
- rev: 5.13.2
- hooks:
- - id: isort
- args: [--filter-files]
- files: \.py$
-# If needed:
- - repo: local
- hooks:
- - id: rst
- name: rst
- entry: rst-lint --encoding utf-8
- exclude: ./doc/.*
- files: .rst
- language: python
- additional_dependencies: [pygments, restructuredtext_lint]
-# This can deal with sphinx directives
- - repo: https://github.com/myint/rstcheck
- rev: "v6.2.4"
- hooks:
- - id: rstcheck
- args: [--ignore-messages, Duplicate implicit target.*]
+ - repo: https://github.com/pycqa/isort
+ rev: "5.13.2"
+ hooks:
+ - id: isort
+ - repo: https://github.com/hhatto/autopep8
+ rev: "v2.3.1"
+ hooks:
+ - id: autopep8
+ args: [--in-place, --aggressive, --aggressive]
+ - repo: https://github.com/asottile/pyupgrade
+ rev: v3.17.0
+ hooks:
+ - id: pyupgrade
+ args: [--py38-plus]
+ - repo: https://github.com/isidentical/teyit
+ rev: 0.4.3
+ hooks:
+ - id: teyit
+ - repo: https://github.com/PyCQA/flake8
+ rev: "7.1.1"
+ hooks:
+ - id: flake8
+ additional_dependencies:
+ - flake8-debugger == 4.1.2
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
new file mode 100644
index 00000000..034043e1
--- /dev/null
+++ b/.readthedocs.yaml
@@ -0,0 +1,25 @@
+# Generated from:
+# https://github.com/zopefoundation/meta/tree/master/config/pure-python
+# Read the Docs configuration file
+# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
+
+# Required
+version: 2
+
+# Set the version of Python and other tools you might need
+build:
+ os: ubuntu-22.04
+ tools:
+ python: "3.11"
+
+# Build documentation in the docs/ directory with Sphinx
+sphinx:
+ configuration: docs/conf.py
+
+# We recommend specifying your dependencies to enable reproducible builds:
+# https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
+python:
+ install:
+ - requirements: docs/requirements.txt
+ - method: pip
+ path: .
diff --git a/CHANGES.rst b/CHANGES.rst
new file mode 100644
index 00000000..69f94a5a
--- /dev/null
+++ b/CHANGES.rst
@@ -0,0 +1,7 @@
+Change log
+==========
+
+1.0 (unreleased)
+----------------
+
+- Converted to an installable Python package.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index bcb80456..31d95f0e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,3 +1,7 @@
+
# Contributing to zopefoundation projects
The projects under the zopefoundation GitHub organization are open source and
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 00000000..98808183
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,21 @@
+# Generated from:
+# https://github.com/zopefoundation/meta/tree/master/config/pure-python
+include *.md
+include *.rst
+include *.txt
+include buildout.cfg
+include tox.ini
+include .pre-commit-config.yaml
+
+recursive-include docs *.py
+recursive-include docs *.rst
+recursive-include docs *.txt
+recursive-include docs Makefile
+
+recursive-include src *.py
+include *.yaml
+recursive-include docs *.bat
+recursive-include src *.j2
+recursive-include src *.md
+recursive-include src *.sh
+recursive-include src *.txt
diff --git a/README.md b/README.md
deleted file mode 100644
index d9ea9376..00000000
--- a/README.md
+++ /dev/null
@@ -1,4 +0,0 @@
-# meta
-Meta issues concerning many/all of the zopefoundation repositories.
-
-See the [Issues](https://github.com/zopefoundation/meta/issues) for details.
diff --git a/README.rst b/README.rst
new file mode 100644
index 00000000..aa922010
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,22 @@
+``zope.meta``
+=============
+
+.. image:: https://img.shields.io/pypi/v/zope.meta.svg
+ :target: https://pypi.python.org/pypi/zope.meta/
+ :alt: Latest Version
+
+.. image:: https://github.com/zopefoundation/meta/actions/workflows/tests.yml/badge.svg
+ :target: https://github.com/zopefoundation/meta/actions/workflows/tests.yml
+
+.. image:: https://coveralls.io/repos/github/zopefoundation/meta/badge.svg?branch=master
+ :target: https://coveralls.io/github/zopefoundation/meta?branch=master
+
+.. image:: https://readthedocs.org/projects/zopemeta/badge/?version=latest
+ :target: https://zopemeta.readthedocs.io/en/latest/?badge=latest
+ :alt: Documentation Status
+
+This package contains helper functions and scripts for maintaining package
+configurations for Zope Foundation GitHub packages
+(https://github.com/zopefoundation).
+
+Please visit https://zopemeta.readthedocs.io for the documentation.
diff --git a/config/multi-call.py b/config/multi-call.py
deleted file mode 100644
index bceae417..00000000
--- a/config/multi-call.py
+++ /dev/null
@@ -1,61 +0,0 @@
-#!/usr/bin/env python3
-##############################################################################
-#
-# Copyright (c) 2020 Zope Foundation and Contributors.
-#
-# This software is subject to the provisions of the Zope Public License,
-# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
-# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
-# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
-# FOR A PARTICULAR PURPOSE.
-#
-##############################################################################
-from shared.call import call
-from shared.packages import list_packages
-from shared.path import change_dir
-from shared.path import path_factory
-import argparse
-import sys
-
-
-parser = argparse.ArgumentParser(
- description='Call a script on all repositories listed in a packages.txt.',
- epilog='Additional optional arguments are passed directly to the script.')
-parser.add_argument(
- 'script', type=path_factory('script', has_extension='.py'),
- help='path to the Python script to be called')
-parser.add_argument(
- 'packages_txt', type=path_factory('packages.txt', has_extension='.txt'),
- help='path to the packages.txt; script is called on each repository listed'
- ' inside', metavar='packages.txt')
-parser.add_argument(
- 'clones', type=path_factory('clones', is_dir=True),
- help='path to the directory where the clones of the repositories are'
- ' stored')
-
-# idea from https://stackoverflow.com/a/37367814/8531312
-args, sub_args = parser.parse_known_args()
-packages = list_packages(args.packages_txt)
-
-for package in packages:
- print(f'*** Running {args.script.name} on {package} ***')
- if (args.clones / package).exists():
- with change_dir(args.clones / package):
- print('Updating existing checkout …')
- call('git', 'stash')
- call('git', 'checkout', 'master')
- call('git', 'pull')
- else:
- with change_dir(args.clones):
- print('Cloning repository …')
- call('git', 'clone',
- f'https://github.com/zopefoundation/{package}')
-
- call_args = [
- sys.executable,
- args.script,
- args.clones / package
- ]
- call_args.extend(sub_args)
- call(*call_args)
diff --git a/config/requirements.txt b/config/requirements.txt
deleted file mode 100644
index 759f2c70..00000000
--- a/config/requirements.txt
+++ /dev/null
@@ -1,8 +0,0 @@
-check-python-versions==0.22.0
-Jinja2==3.1.4
-packaging==24.1
-pyupgrade==3.16.0
-requests==2.32.3
-tomlkit==0.13.2
-tox==4.18.1
-zest.releaser==9.2.0
diff --git a/config/update-python-support.py b/config/update-python-support.py
deleted file mode 100644
index 398f3f54..00000000
--- a/config/update-python-support.py
+++ /dev/null
@@ -1,168 +0,0 @@
-#!/usr/bin/env python3
-##############################################################################
-#
-# Copyright (c) 2022 Zope Foundation and Contributors.
-#
-# This software is subject to the provisions of the Zope Public License,
-# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
-# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
-# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
-# FOR A PARTICULAR PURPOSE.
-#
-##############################################################################
-from shared.call import call
-from shared.call import wait_for_accept
-from shared.git import get_branch_name
-from shared.git import git_branch
-from shared.packages import OLDEST_PYTHON_VERSION
-from shared.packages import supported_python_versions
-from shared.path import change_dir
-import argparse
-import collections
-import configparser
-import os
-import pathlib
-import shutil
-import sys
-import tomlkit
-
-
-def get_tox_ini_python_versions(path):
- config = configparser.ConfigParser()
- config.read(path)
- envs = config['tox']['envlist'].split()
- versions = [
- env.replace('py', '').replace('3', '3.') for env in envs
- if env.startswith('py') and env != 'pypy3'
- ]
- return versions
-
-
-parser = argparse.ArgumentParser(
- description='Update Python versions of a package to currently supported'
- ' ones.')
-parser.add_argument('path',
- type=pathlib.Path,
- help='path to the repository to be configured')
-parser.add_argument(
- '--branch',
- dest='branch_name',
- default=None,
- help='Define a git branch name to be used for the changes. If not given'
- ' it is constructed automatically and includes the configuration'
- ' type')
-parser.add_argument(
- '--no-commit',
- dest='commit',
- action='store_false',
- default=True,
- help='Don\'t "git commit" changes made by this script.')
-parser.add_argument(
- '--interactive',
- dest='interactive',
- action='store_true',
- default=False,
- help='Run interactively: Scripts will prompt for input. Implies '
- '--no-commit, changes will not be committed and pushed automatically.')
-
-args = parser.parse_args()
-path = args.path.absolute()
-
-if not (path / '.git').exists():
- raise ValueError('`path` does not point to a git clone of a repository!')
-if not (path / '.meta.toml').exists():
- raise ValueError('The repository `path` points to has no .meta.toml!')
-
-with change_dir(path) as cwd_str:
- cwd = pathlib.Path(cwd_str)
- bin_dir = cwd / 'bin'
- with open('.meta.toml', 'rb') as meta_f:
- meta_toml = collections.defaultdict(dict, **tomlkit.load(meta_f))
- config_type = meta_toml['meta']['template']
- 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))
-
- non_interactive_params = []
- if not args.interactive and args.commit:
- non_interactive_params = ['--no-input']
- else:
- args.commit = False
-
- if no_longer_supported or not_yet_supported:
- call(bin_dir / 'bumpversion', '--feature', *non_interactive_params)
- python_versions_args = ['--add=' + ','.join(supported_python_versions())]
- 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)
- 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)
-
- call(bin_dir / 'check-python-versions', '--only=setup.py',
- *python_versions_args)
- print('Look trough .meta.toml to see if it needs changes.')
- call(os.environ['EDITOR'], '.meta.toml')
-
- config_package_args = [
- sys.executable,
- 'config-package.py',
- path,
- f'--branch={branch_name}',
- '--no-push',
- ]
- if not args.commit:
- config_package_args.append('--no-commit')
- call(*config_package_args, cwd=cwd_str)
- src = path.resolve() / 'src'
- py_version_plus = f'--py{OLDEST_PYTHON_VERSION.replace(".", "")}-plus'
- call('find', src, '-name', '*.py', '-exec', bin_dir / 'pyupgrade',
- '--py3-plus', py_version_plus, '{}', ';')
- call(bin_dir / 'pyupgrade',
- '--py3-plus',
- py_version_plus,
- 'setup.py',
- allowed_return_codes=(0, 1))
-
- excludes = ('--exclude-dir', '__pycache__', '--exclude-dir', '*.egg-info',
- '--exclude', '*.pyc', '--exclude', '*.so')
- print('Replace any remaining code that might support legacy Python:')
- call('egrep',
- '-rn', f'{"|".join(no_longer_supported)}|sys.version|PY3|Py3|Python 3'
- '|__unicode__|ImportError',
- src,
- *excludes,
- allowed_return_codes=(0, 1))
- wait_for_accept()
- tox_path = shutil.which('tox') or (cwd / 'bin' / 'tox')
- call(tox_path, '-p', 'auto')
- if args.commit:
- print('Adding, committing and pushing all changes ...')
- call('git', 'add', '.')
- call('git', 'commit', '-m', 'Update Python version support.')
- call('git', 'push', '--set-upstream', 'origin', branch_name)
- if updating:
- print('Updated the previously created PR.')
- else:
- print(
- 'Are you logged in via `gh auth login` to automatically'
- ' create a PR? (y/N)?',
- end=' ')
- if input().lower() == 'y':
- call('gh', 'pr', 'create', '--fill', '--title',
- 'Update Python version support.')
- else:
- print('If everything went fine up to here:')
- print('Create a PR, using the URL shown above.')
- else:
- print('Applied all changes. Please check and commit manually.')
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 00000000..d4bb2cbb
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,20 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line, and also
+# from the environment for the first two.
+SPHINXOPTS ?=
+SPHINXBUILD ?= sphinx-build
+SOURCEDIR = .
+BUILDDIR = _build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/docs/api.rst b/docs/api.rst
new file mode 100644
index 00000000..6cfcb3cf
--- /dev/null
+++ b/docs/api.rst
@@ -0,0 +1,2 @@
+:mod:`zope.meta` API Reference
+==============================
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 00000000..538bb613
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,40 @@
+# Configuration file for the Sphinx documentation builder.
+#
+# For the full list of built-in configuration values, see the documentation:
+# https://www.sphinx-doc.org/en/master/usage/configuration.html
+
+import datetime
+
+
+# -- Project information -----------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
+year = datetime.datetime.now().year
+
+project = 'zope.meta'
+copyright = f'2020-{year}, Zope Foundation and contributors'
+author = 'Zope Foundation and contributors'
+
+# -- General configuration ---------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
+
+extensions = [
+ 'sphinx.ext.autodoc',
+ 'sphinx.ext.intersphinx',
+]
+
+# templates_path = ['_templates']
+exclude_patterns = []
+
+
+# -- Options for HTML output -------------------------------------------------
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
+
+html_theme = 'furo'
+# html_static_path = ['_static']
+
+# Example configuration for intersphinx: refer to the Python standard library.
+intersphinx_mapping = {
+ 'python': ('https://docs.python.org/3', None),
+ 'testrunner': ('https://zopetestrunner.readthedocs.io/en/stable/', None),
+ 'coverage': ('https://coverage.readthedocs.io/en/latest', None),
+}
diff --git a/docs/hacking.rst b/docs/hacking.rst
new file mode 100644
index 00000000..0bf7b7fc
--- /dev/null
+++ b/docs/hacking.rst
@@ -0,0 +1,192 @@
+Hacking on :mod:`zope.meta`
+===========================
+
+
+Getting the Code
+################
+
+The main repository for :mod:`zope.meta` is in the Zope Foundation
+Github repository:
+
+ https://github.com/zopefoundation/meta
+
+You can get a read-only checkout from there:
+
+.. code-block:: sh
+
+ $ git clone https://github.com/zopefoundation/meta.git
+
+or fork it and get a writeable checkout of your fork:
+
+.. code-block:: sh
+
+ $ git clone git@github.com/jrandom/meta.git
+
+
+Working in a Python virtual environment
+#######################################
+
+Installing
+----------
+
+You can use Python's standard ``venv`` package to create lightweight Python
+development environments, where you can run the tests using nothing more
+than the ``python`` binary in a virtualenv. First, create a scratch
+environment:
+
+.. code-block:: sh
+
+ $ python3.12 -m venv /tmp/hack-zope.meta
+
+Next, install this package in "development mod" in the newly-created
+environment:
+
+.. code-block:: sh
+
+ $ /tmp/hack-zope.meta/bin/pip install -e .
+
+Running the tests
+-----------------
+
+You can install test tools using the ``test`` extra:
+
+.. code-block:: sh
+
+ $ /tmp/hack-zope.meta/bin/pip install -e ".[test]"
+
+
+That command installs the tools needed to run
+the tests: in particular, the ``zope.testrunner`` (see
+:external+testrunner:std:doc:`getting-started`) and
+:external+coverage:std:doc:`index` tools.
+
+To run the tests via ``zope.testrunner``:
+
+.. code-block:: sh
+
+ $ /tmp/hack-zope.meta/bin/zope-testrunner --test-path=src
+ Running zope.testrunner.layer.UnitTests tests:
+ ...
+
+Running the tests under :mod:`coverage` lets you see how well the tests
+cover the code:
+
+.. code-block:: sh
+
+ $ /tmp/hack-zope.meta/bin/coverage run -m zope.testrunner \
+ --test-path=src
+ ...
+ $ coverage report -i -m --fail-under=100
+ Name Stmts Miss Branch BrPart Cover Missing
+ ----------------------------------------------------------------------------------
+ ...
+
+
+Building the documentation
+--------------------------
+
+:mod:`zope.meta` uses the nifty :mod:`Sphinx` documentation system
+for building its docs. Using the same virtualenv you set up to run the
+tests, you can build the docs:
+
+The ``docs`` command alias downloads and installs Sphinx and its dependencies:
+
+.. code-block:: sh
+
+ $ /tmp/hack-zope.meta/bin/pip install ".[docs]"
+ ...
+ $ /tmp/hack-zope.meta/bin/sphinx-build -b html -d docs/_build/doctrees docs docs/_build/html
+ ...
+ build succeeded.
+
+ The HTML pages are in docs/_build/html.
+
+
+Using :mod:`tox`
+################
+
+
+Running Tests on Multiple Python Versions
+-----------------------------------------
+
+`tox `_ is a Python-based test automation
+tool designed to run tests against multiple Python versions. It creates
+a virtual environment for each configured version, installs the current
+package and configured dependencies into each environment, and then runs the
+configured commands.
+
+:mod:`zope.meta` configures the following :mod:`tox` environments via
+its ``tox.ini`` file:
+
+- The ``lint`` environment runs various "code quality" tests on the source,
+ and fails on any errors they find.
+
+- The ``pyXX`` and ``pypy3`` environments each build an environment from the
+ corresponding
+ Python version, install :mod:`zope.meta` and testing dependencies,
+ and runs the tests. It then installs ``Sphinx`` and runs the doctest
+ snippets.
+
+- The ``coverage`` environment builds a virtual environment,
+ installs :mod:`zope.meta` and dependencies, installs
+ :mod:`coverage`, and runs the tests with statement and branch
+ coverage.
+
+- The ``docs`` environment builds a virtual environment, installs
+ :mod:`zope.meta` and dependencies, installs ``Sphinx`` and
+ dependencies, and then builds the docs and exercises the doctest snippets.
+
+This example requires that you have a working ``python3.12`` on your path,
+as well as installing ``tox``:
+
+.. code-block:: sh
+
+ $ tox -e py312
+ py312: install_deps> python -I -m pip install 'setuptools<74' Sphinx
+ ...
+ py312: commands[0]> zope-testrunner --test-path=src -vc
+ Running tests at level 1
+ Running zope.testrunner.layer.UnitTests tests:
+ Set up zope.testrunner.layer.UnitTests in 0.000 seconds.
+ Running:
+ .....
+
+Running ``tox`` with no arguments runs all the configured environments,
+including building the docs and testing their snippets.
+
+
+Contributing to :mod:`zope.meta`
+################################
+
+Submitting a Bug Report
+-----------------------
+
+:mod:`zope.meta` tracks its bugs on Github:
+
+ https://github.com/zopefoundation/meta/issues
+
+Please submit bug reports and feature requests there.
+
+Sharing Your Changes
+--------------------
+
+.. note::
+
+ Please ensure that all tests are passing before you submit your code.
+ If possible, your submission should include new tests for new features
+ or bug fixes, although it is possible that you may have tested your
+ new code by updating existing tests.
+
+ Contributions to Plone/Zope Foundation packages require contributor status.
+ Please see https://www.zope.dev/developer/becoming-a-committer.html.
+
+If you have made changes you would like to share, the best route is to create a
+branch in the GitHub repository and push changes there, which requires
+`contributor status
+`_. You can
+also fork the GitHub repository, check out your fork, make your changes on a
+branch in your fork, and then push them. A private fork makes it harder for
+others and the package maintainers to work with your changes, so it is
+discouraged. Either way, you can then submit a pull request from your branch:
+
+ https://github.com/zopefoundation/meta/pulls
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 00000000..0a3399b2
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,24 @@
+:mod:`zope.meta` Documentation
+==============================
+
+This package provides helper functions and scripts to maintain package
+configurations for the Zope Foundation packages.
+
+Contents:
+
+.. toctree::
+ :maxdepth: 2
+
+ narr
+ api
+ hacking
+
+
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
+
diff --git a/docs/make.bat b/docs/make.bat
new file mode 100644
index 00000000..954237b9
--- /dev/null
+++ b/docs/make.bat
@@ -0,0 +1,35 @@
+@ECHO OFF
+
+pushd %~dp0
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
+set SOURCEDIR=.
+set BUILDDIR=_build
+
+%SPHINXBUILD% >NUL 2>NUL
+if errorlevel 9009 (
+ echo.
+ echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+ echo.installed, then set the SPHINXBUILD environment variable to point
+ echo.to the full path of the 'sphinx-build' executable. Alternatively you
+ echo.may add the Sphinx directory to PATH.
+ echo.
+ echo.If you don't have Sphinx installed, grab it from
+ echo.https://www.sphinx-doc.org/
+ exit /b 1
+)
+
+if "%1" == "" goto help
+
+%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+goto end
+
+:help
+%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+
+:end
+popd
diff --git a/config/README.rst b/docs/narr.rst
similarity index 93%
rename from config/README.rst
rename to docs/narr.rst
index db5c5792..e4269977 100644
--- a/config/README.rst
+++ b/docs/narr.rst
@@ -1,6 +1,6 @@
-======
-Config
-======
+Using :mod:`zope.meta`
+======================
+
Purpose
-------
@@ -9,8 +9,8 @@ Bring the configuration of the zopefoundation packages into a common state and
keep it there.
-Types
------
+Configuration types
+-------------------
This directory contains the configuration directories for different types of
packages:
@@ -39,11 +39,12 @@ packages:
- Configuration used for the zopetoolkit and groktoolkit repositories.
-Contents
---------
-Each directory contains the following files if they differ from the default
-(stored in a directory named ``default``):
+Configuration templates
+-----------------------
+
+Each configuration type folder can override the default configuration in the
+folder ``default`` by providing one or more of these files:
* packages.txt
@@ -90,20 +91,23 @@ Each directory contains the following files if they differ from the default
- Configuration for GitHub actions.
-Usage
------
+The ``config-package`` script
+-----------------------------
+
+The ``config-package`` script applies package configuration in a given Python
+package.
Preparation
+++++++++++
-The script needs a ``venv`` with some packages installed::
+The scripts needs a ``venv`` with some packages installed::
$ python3.11 -m venv .
- $ bin/pip install -r requirements.txt
+ $ bin/pip install .
To use the configuration provided here in a package call the following script::
- $ bin/python config-package.py --type []
+ $ bin/config-package --type []
See ``--help`` for details.
@@ -155,8 +159,8 @@ The following options are only needed one time as their values are stored in
``.meta.toml.``.
--type
- Define the configuration type (see `Types`_ section above) to be used for the
- repository.
+ Define the configuration type (see `Configuration types`_ section above) to
+ be used for the repository.
--with-macos
Enable running the tests on macOS on GitHub Actions.
@@ -180,6 +184,7 @@ The following options are only needed one time as their values are stored in
--with-sphinx-doctests
Enable running the documentation as doctest using Sphinx.
+
Options
+++++++
@@ -402,6 +407,7 @@ source
section. This option has to be a string. It defaults to the name of the
package if it is not set.
+
tox.ini options
```````````````
@@ -466,6 +472,7 @@ docs-deps
and is empty by default. Caution: The values set for this option override
the ones set in ``[testenv]``.
+
Flake8 options
``````````````
@@ -506,6 +513,7 @@ ignore-bad-ideas
Ignore bad idea files/directories matching these patterns. This option has to
be a list of strings.
+
Isort options
`````````````
@@ -546,6 +554,7 @@ additional-config
Additional options for the ``[isort]`` section. This option has to be a
list of strings.
+
GitHub Actions options
``````````````````````
@@ -608,6 +617,7 @@ require-cffi
is needed for some packages to circumvent build problems on MacOS. This
option has to be a boolean (true or false).
+
zest.releaser options
`````````````````````
@@ -618,6 +628,7 @@ options
(Additional) options used to configure ``zest.releaser``. This option has to
be a list of strings and defaults to an empty list.
+
git options
```````````
@@ -627,6 +638,7 @@ ignore
Additional lines to be added to the ``.gitignore`` file. This option has to
be a list of strings and defaults to an empty list.
+
pre-commit options
``````````````````
@@ -636,6 +648,7 @@ teyit-exclude
Regex for files to be hidden from teyit. It fails on files containing syntax
errors. This option has to be a string and is omitted when not defined.
+
ReadTheDocs options
```````````````````
@@ -646,49 +659,56 @@ build-extra
ReadTheDocs configuration file ``.readthedocs.yaml``. This option has to
be a list of strings and defaults to an empty list.
-Hints
------
-* Calling ``config-package.py`` again updates a previously created pull request
- if there are changes made in the files ``config-package.py`` touches.
+Configuration script hints
+--------------------------
+
+* Calling ``config-package`` again updates a previously created pull request
+ if there are changes made in the files ``config-package`` touches.
* Call ``bin/check-python-versions -h`` to see how to fix
version mismatches in the *lint* tox environment.
+
+Other helper scripts
+====================
+
Updating to the currently supported Python versions
---------------------------------------------------
-There is `update-python-support.py` which can be used to update a repository to
+There is a script `update-python-support` for updating a repository to
the currently supported Python versions as defined in ``shared/package.py``.
+
Usage
+++++
To update a repository to the currently supported Python versions call::
- $ bin/python update-python-support.py
+ $ bin/update-python-support
It supports a parameter ``--interactive`` to gather user input for its changes
and not automatically commit them. It also supports a parameter ``--no-commit``
that prevents automatic commits but attempts to cut down on interactively
asking for user input. Some of that still happens due to limitations
-of the ``zest.releaser`` scripts used by ``update-python-support.py``.
+of the ``zest.releaser`` scripts used by ``update-python-support``.
Calling a script on multiple repositories
-----------------------------------------
-The ``config-package.py`` script only runs on a single repository. To update
-multiple repositories at once you can use ``multi-call.py``. It runs a given
+The ``config-package`` script only runs on a single repository. To update
+multiple repositories at once you can use ``multi-call``. It runs a given
script on all repositories listed in a ``packages.txt`` file.
+
Usage
+++++
To run a script on all packages listed in a ``packages.txt`` file call
-``multi-call.py`` the following way::
+``multi-call`` the following way::
- $ bin/python multi-call.py
+ $ bin/multi-call
See ``--help`` for details.
@@ -715,6 +735,7 @@ automatically disables Actions. They can be re-enabled manually per repository.
There is a script to do this for all repositories. It does no harm if Actions
is already enabled for a repository.
+
Preparation
+++++++++++
@@ -725,9 +746,10 @@ Preparation
- ``gh auth login``
- It is probably enough to do it once.
+
Usage
+++++
To run the script just call it::
- $ bin/python re-enable-actions.py
+ $ bin/re-enable-actions
diff --git a/docs/requirements.txt b/docs/requirements.txt
new file mode 100644
index 00000000..de8fca92
--- /dev/null
+++ b/docs/requirements.txt
@@ -0,0 +1,2 @@
+Sphinx
+furo
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 00000000..02e46908
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,21 @@
+#
+# Generated from:
+# https://github.com/zopefoundation/meta/tree/master/config/pure-python
+
+[build-system]
+requires = ["setuptools<74"]
+build-backend = "setuptools.build_meta"
+
+[tool.coverage.run]
+branch = true
+source = ["zope.meta"]
+
+[tool.coverage.report]
+fail_under = 20
+precision = 2
+ignore_errors = true
+show_missing = true
+exclude_lines = ["pragma: no cover", "pragma: nocover", "except ImportError:", "raise NotImplementedError", "if __name__ == '__main__':", "self.fail", "raise AssertionError", "raise unittest.Skip"]
+
+[tool.coverage.html]
+directory = "parts/htmlcov"
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 00000000..fc847675
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,23 @@
+# Generated from:
+# https://github.com/zopefoundation/meta/tree/master/config/pure-python
+
+[flake8]
+doctests = 1
+
+[check-manifest]
+ignore =
+ .editorconfig
+ .meta.toml
+ docs/_build/html/_sources/*
+ docs/_build/html/_static/scripts/*
+
+[isort]
+force_single_line = True
+combine_as_imports = True
+sections = FUTURE,STDLIB,THIRDPARTY,ZOPE,FIRSTPARTY,LOCALFOLDER
+known_third_party = docutils, pkg_resources, pytz
+known_zope =
+known_first_party =
+default_section = ZOPE
+line_length = 79
+lines_after_imports = 2
diff --git a/setup.py b/setup.py
new file mode 100644
index 00000000..f46a2596
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,97 @@
+##############################################################################
+#
+# Copyright (c) 2024 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Setup for zope.meta package
+"""
+
+import os
+
+from setuptools import find_packages
+from setuptools import setup
+
+
+def read(*rnames):
+ with open(os.path.join(os.path.dirname(__file__), *rnames)) as stream:
+ return stream.read()
+
+
+setup(
+ name='zope.meta',
+ version='1.0.dev0',
+ author='Zope Foundation and Contributors',
+ author_email='zope-dev@zope.dev',
+ description='Helper functions for package management',
+ long_description=(
+ read('README.rst')
+ + '\n\n' +
+ read('CHANGES.rst')
+ ),
+ long_description_content_type='text/x-rst',
+ keywords="zope packaging",
+ classifiers=[
+ 'Development Status :: 3 - Alpha',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: Zope Public License',
+ 'Programming Language :: Python',
+ 'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 3.8',
+ 'Programming Language :: Python :: 3.9',
+ 'Programming Language :: Python :: 3.10',
+ 'Programming Language :: Python :: 3.11',
+ 'Programming Language :: Python :: 3.12',
+ 'Programming Language :: Python :: 3.13',
+ 'Programming Language :: Python :: Implementation :: CPython',
+ 'Natural Language :: English',
+ 'Operating System :: OS Independent',
+ ],
+ license='ZPL 2.1',
+ url='https://github.com/zopefoundation/meta',
+ project_urls={
+ 'Documentation': 'https://zopemeta.readthedocs.io',
+ 'Issue Tracker': 'https://github.com/zopefoundation/meta/issues',
+ 'Sources': 'https://github.com/zopefoundation/meta',
+ },
+ packages=find_packages('src'),
+ package_dir={'': 'src'},
+ namespace_packages=['zope'],
+ install_requires=[
+ 'setuptools',
+ 'check-python-versions',
+ 'Jinja2',
+ 'packaging',
+ 'pyupgrade',
+ 'requests',
+ 'tomlkit',
+ 'tox',
+ 'zest.releaser',
+ ],
+ python_requires='>=3.8',
+ include_package_data=True,
+ zip_safe=False,
+ extras_require={
+ 'test': ['zope.testrunner'],
+ 'docs': ['Sphinx', 'furo'],
+ },
+ entry_points={
+ 'console_scripts': [
+ 'config-package=zope.meta.config_package:main',
+ 'multi-call=zope.meta.multi_call:main',
+ 're-enable-actions=zope.meta.re_enable_actions:main',
+ (
+ 'set-branch-protection-rules='
+ 'zope.meta.set_branch_protection_rules:main'
+ ),
+ 'update-python-support=zope.meta.update_python_support:main',
+ ],
+ },
+)
diff --git a/src/zope/__init__.py b/src/zope/__init__.py
new file mode 100644
index 00000000..656dc0f7
--- /dev/null
+++ b/src/zope/__init__.py
@@ -0,0 +1 @@
+__import__('pkg_resources').declare_namespace(__name__) # pragma: no cover
diff --git a/src/zope/meta/__init__.py b/src/zope/meta/__init__.py
new file mode 100644
index 00000000..4066b630
--- /dev/null
+++ b/src/zope/meta/__init__.py
@@ -0,0 +1,15 @@
+##############################################################################
+#
+# Copyright (c) 2024 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Zope Foundation packaging helpers
+"""
diff --git a/config/buildout-recipe/coveragerc.j2 b/src/zope/meta/buildout-recipe/coveragerc.j2
similarity index 100%
rename from config/buildout-recipe/coveragerc.j2
rename to src/zope/meta/buildout-recipe/coveragerc.j2
diff --git a/config/buildout-recipe/packages.txt b/src/zope/meta/buildout-recipe/packages.txt
similarity index 100%
rename from config/buildout-recipe/packages.txt
rename to src/zope/meta/buildout-recipe/packages.txt
diff --git a/config/buildout-recipe/tox.ini.j2 b/src/zope/meta/buildout-recipe/tox.ini.j2
similarity index 100%
rename from config/buildout-recipe/tox.ini.j2
rename to src/zope/meta/buildout-recipe/tox.ini.j2
diff --git a/config/c-code/coveragerc.j2 b/src/zope/meta/c-code/coveragerc.j2
similarity index 100%
rename from config/c-code/coveragerc.j2
rename to src/zope/meta/c-code/coveragerc.j2
diff --git a/config/c-code/manylinux-install.sh.j2 b/src/zope/meta/c-code/manylinux-install.sh.j2
similarity index 100%
rename from config/c-code/manylinux-install.sh.j2
rename to src/zope/meta/c-code/manylinux-install.sh.j2
diff --git a/config/c-code/manylinux.sh b/src/zope/meta/c-code/manylinux.sh
similarity index 100%
rename from config/c-code/manylinux.sh
rename to src/zope/meta/c-code/manylinux.sh
diff --git a/config/c-code/packages.txt b/src/zope/meta/c-code/packages.txt
similarity index 100%
rename from config/c-code/packages.txt
rename to src/zope/meta/c-code/packages.txt
diff --git a/config/c-code/tests-cache.j2 b/src/zope/meta/c-code/tests-cache.j2
similarity index 100%
rename from config/c-code/tests-cache.j2
rename to src/zope/meta/c-code/tests-cache.j2
diff --git a/config/c-code/tests-download.j2 b/src/zope/meta/c-code/tests-download.j2
similarity index 100%
rename from config/c-code/tests-download.j2
rename to src/zope/meta/c-code/tests-download.j2
diff --git a/config/c-code/tests-strategy.j2 b/src/zope/meta/c-code/tests-strategy.j2
similarity index 100%
rename from config/c-code/tests-strategy.j2
rename to src/zope/meta/c-code/tests-strategy.j2
diff --git a/config/c-code/tests.yml.j2 b/src/zope/meta/c-code/tests.yml.j2
similarity index 100%
rename from config/c-code/tests.yml.j2
rename to src/zope/meta/c-code/tests.yml.j2
diff --git a/config/c-code/tox.ini.j2 b/src/zope/meta/c-code/tox.ini.j2
similarity index 100%
rename from config/c-code/tox.ini.j2
rename to src/zope/meta/c-code/tox.ini.j2
diff --git a/config/config-package.py b/src/zope/meta/config_package.py
old mode 100755
new mode 100644
similarity index 96%
rename from config/config-package.py
rename to src/zope/meta/config_package.py
index 18442356..607bf5cc
--- a/config/config-package.py
+++ b/src/zope/meta/config_package.py
@@ -11,33 +11,35 @@
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
-from functools import cached_property
-from set_branch_protection_rules import set_branch_protection
-from shared.call import abort
-from shared.call import call
-from shared.git import get_branch_name
-from shared.git import get_commit_id
-from shared.git import git_branch
-from shared.packages import FUTURE_PYTHON_VERSION
-from shared.packages import MANYLINUX_AARCH64
-from shared.packages import MANYLINUX_I686
-from shared.packages import MANYLINUX_PYTHON_VERSION
-from shared.packages import MANYLINUX_X86_64
-from shared.packages import NEWEST_PYTHON_VERSION
-from shared.packages import OLDEST_PYTHON_VERSION
-from shared.packages import PYPY_VERSION
-from shared.packages import SETUPTOOLS_VERSION_SPEC
-from shared.packages import get_pyproject_toml_defaults
-from shared.packages import parse_additional_config
-from shared.packages import supported_python_versions
-from shared.path import change_dir
import argparse
import collections
-import jinja2
import pathlib
import shutil
+from functools import cached_property
+
+import jinja2
import tomlkit
+from .set_branch_protection_rules import set_branch_protection
+from .shared.call import abort
+from .shared.call import call
+from .shared.git import get_branch_name
+from .shared.git import get_commit_id
+from .shared.git import git_branch
+from .shared.packages import FUTURE_PYTHON_VERSION
+from .shared.packages import MANYLINUX_AARCH64
+from .shared.packages import MANYLINUX_I686
+from .shared.packages import MANYLINUX_PYTHON_VERSION
+from .shared.packages import MANYLINUX_X86_64
+from .shared.packages import NEWEST_PYTHON_VERSION
+from .shared.packages import OLDEST_PYTHON_VERSION
+from .shared.packages import PYPY_VERSION
+from .shared.packages import SETUPTOOLS_VERSION_SPEC
+from .shared.packages import get_pyproject_toml_defaults
+from .shared.packages import parse_additional_config
+from .shared.packages import supported_python_versions
+from .shared.path import change_dir
+
FUTURE_PYTHON_SHORTVERSION = FUTURE_PYTHON_VERSION.replace('.', '')
NEWEST_PYTHON_SHORTVERSION = NEWEST_PYTHON_VERSION.replace('.', '')
@@ -146,7 +148,7 @@ def handle_command_line_arguments():
def prepend_space(text):
- """Prepend `text` which a space if not empty.
+ """Prepend `text` with a space if not empty.
This prevents trailing whitespace for empty values.
"""
@@ -632,7 +634,7 @@ def configure(self):
self.gitignore()
self.pre_commit_config_yaml()
self.copy_with_meta(
- 'editorconfig', self.path / '.editorconfig', self.config_type)
+ 'editorconfig.txt', self.path / '.editorconfig', self.config_type)
self.copy_with_meta(
'CONTRIBUTING.md', self.path / 'CONTRIBUTING.md', self.config_type,
meta_hint=META_HINT_MARKDOWN)
@@ -734,6 +736,3 @@ def main():
package = PackageConfiguration(args)
package.configure()
-
-
-main()
diff --git a/config/default/CONTRIBUTING.md b/src/zope/meta/default/CONTRIBUTING.md
similarity index 100%
rename from config/default/CONTRIBUTING.md
rename to src/zope/meta/default/CONTRIBUTING.md
diff --git a/config/default/MANIFEST.in.j2 b/src/zope/meta/default/MANIFEST.in.j2
similarity index 100%
rename from config/default/MANIFEST.in.j2
rename to src/zope/meta/default/MANIFEST.in.j2
diff --git a/config/default/editorconfig b/src/zope/meta/default/editorconfig.txt
similarity index 100%
rename from config/default/editorconfig
rename to src/zope/meta/default/editorconfig.txt
diff --git a/config/default/gitignore.j2 b/src/zope/meta/default/gitignore.j2
similarity index 100%
rename from config/default/gitignore.j2
rename to src/zope/meta/default/gitignore.j2
diff --git a/config/default/pre-commit-config.yaml.j2 b/src/zope/meta/default/pre-commit-config.yaml.j2
similarity index 100%
rename from config/default/pre-commit-config.yaml.j2
rename to src/zope/meta/default/pre-commit-config.yaml.j2
diff --git a/config/default/pre-commit.yml.j2 b/src/zope/meta/default/pre-commit.yml.j2
similarity index 100%
rename from config/default/pre-commit.yml.j2
rename to src/zope/meta/default/pre-commit.yml.j2
diff --git a/config/default/readthedocs.yaml.j2 b/src/zope/meta/default/readthedocs.yaml.j2
similarity index 100%
rename from config/default/readthedocs.yaml.j2
rename to src/zope/meta/default/readthedocs.yaml.j2
diff --git a/config/default/setup.cfg.j2 b/src/zope/meta/default/setup.cfg.j2
similarity index 100%
rename from config/default/setup.cfg.j2
rename to src/zope/meta/default/setup.cfg.j2
diff --git a/config/default/tests.yml.j2 b/src/zope/meta/default/tests.yml.j2
similarity index 100%
rename from config/default/tests.yml.j2
rename to src/zope/meta/default/tests.yml.j2
diff --git a/config/default/tox-docs.j2 b/src/zope/meta/default/tox-docs.j2
similarity index 100%
rename from config/default/tox-docs.j2
rename to src/zope/meta/default/tox-docs.j2
diff --git a/config/default/tox-envlist.j2 b/src/zope/meta/default/tox-envlist.j2
similarity index 100%
rename from config/default/tox-envlist.j2
rename to src/zope/meta/default/tox-envlist.j2
diff --git a/config/default/tox-lint.j2 b/src/zope/meta/default/tox-lint.j2
similarity index 100%
rename from config/default/tox-lint.j2
rename to src/zope/meta/default/tox-lint.j2
diff --git a/config/default/tox-release-check.j2 b/src/zope/meta/default/tox-release-check.j2
similarity index 100%
rename from config/default/tox-release-check.j2
rename to src/zope/meta/default/tox-release-check.j2
diff --git a/config/default/tox-testenv.j2 b/src/zope/meta/default/tox-testenv.j2
similarity index 100%
rename from config/default/tox-testenv.j2
rename to src/zope/meta/default/tox-testenv.j2
diff --git a/src/zope/meta/multi_call.py b/src/zope/meta/multi_call.py
new file mode 100644
index 00000000..8fb66c5e
--- /dev/null
+++ b/src/zope/meta/multi_call.py
@@ -0,0 +1,69 @@
+#!/usr/bin/env python3
+##############################################################################
+#
+# Copyright (c) 2020 Zope Foundation and Contributors.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+import argparse
+import sys
+
+from .shared.call import call
+from .shared.packages import list_packages
+from .shared.path import change_dir
+from .shared.path import path_factory
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description='Call a script on all repositories listed'
+ ' in a packages.txt.',
+ epilog='Additional optional arguments are passed'
+ ' directly to the script.')
+ parser.add_argument(
+ 'script', type=path_factory('script', has_extension='.py'),
+ help='path to the Python script to be called')
+ parser.add_argument(
+ 'packages_txt',
+ type=path_factory(
+ 'packages.txt',
+ has_extension='.txt'),
+ help='path to packages.txt; script is called on each repository listed'
+ ' inside',
+ metavar='packages.txt')
+ parser.add_argument(
+ 'clones', type=path_factory('clones', is_dir=True),
+ help='path to the directory where the clones of the repositories are'
+ ' stored')
+
+ # idea from https://stackoverflow.com/a/37367814/8531312
+ args, sub_args = parser.parse_known_args()
+ packages = list_packages(args.packages_txt)
+
+ for package in packages:
+ print(f'*** Running {args.script.name} on {package} ***')
+ if (args.clones / package).exists():
+ with change_dir(args.clones / package):
+ print('Updating existing checkout …')
+ call('git', 'stash')
+ call('git', 'checkout', 'master')
+ call('git', 'pull')
+ else:
+ with change_dir(args.clones):
+ print('Cloning repository …')
+ call('git', 'clone',
+ f'https://github.com/zopefoundation/{package}')
+
+ call_args = [
+ sys.executable,
+ args.script,
+ args.clones / package
+ ]
+ call_args.extend(sub_args)
+ call(*call_args)
diff --git a/config/pure-python/packages.txt b/src/zope/meta/pure-python/packages.txt
similarity index 99%
rename from config/pure-python/packages.txt
rename to src/zope/meta/pure-python/packages.txt
index a493cfe3..55e98a62 100644
--- a/config/pure-python/packages.txt
+++ b/src/zope/meta/pure-python/packages.txt
@@ -208,3 +208,5 @@ zope.contentprovider
z3c.jbot
fanstatic
zope.pytestlayer
+zope.meta
+meta
diff --git a/config/pure-python/tox.ini.j2 b/src/zope/meta/pure-python/tox.ini.j2
similarity index 100%
rename from config/pure-python/tox.ini.j2
rename to src/zope/meta/pure-python/tox.ini.j2
diff --git a/config/re-enable-actions.py b/src/zope/meta/re_enable_actions.py
similarity index 53%
rename from config/re-enable-actions.py
rename to src/zope/meta/re_enable_actions.py
index fadc7630..e239c3a9 100644
--- a/config/re-enable-actions.py
+++ b/src/zope/meta/re_enable_actions.py
@@ -11,28 +11,18 @@
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
-from shared.call import call
-from shared.packages import ALL_REPOS
-from shared.packages import ORG
import argparse
import pathlib
+from .shared.call import call
+from .shared.packages import ALL_REPOS
+from .shared.packages import ORG
+
base_url = f'https://github.com/{ORG}'
BASE_PATH = pathlib.Path(__file__).parent
-parser = argparse.ArgumentParser(
- description='Re-enable GitHub Actions for all repos in a packages.txt'
- ' files.')
-parser.add_argument(
- '--force-run',
- help='Run workflow even it is already enabled.',
- action='store_true')
-
-args = parser.parse_args()
-
-
def run_workflow(base_url, org, repo):
"""Manually start the tests.yml workflow of a repository."""
result = call('gh', 'workflow', 'run', 'tests.yml', '-R', f'{org}/{repo}')
@@ -45,18 +35,29 @@ def run_workflow(base_url, org, repo):
return True
-for repo in ALL_REPOS:
- print(repo)
- wfs = call(
- 'gh', 'workflow', 'list', '--all', '-R', f'{ORG}/{repo}',
- capture_output=True).stdout
- test_line = [x for x in wfs.splitlines() if x.startswith('test')][0]
- if 'disabled_inactivity' not in test_line:
- print(' ☑️ already enabled')
- if args.force_run:
- run_workflow(base_url, ORG, repo)
- continue
- test_id = test_line.split()[-1]
- call('gh', 'workflow', 'enable', test_id, '-R', f'{ORG}/{repo}')
- if run_workflow(base_url, ORG, repo):
- print(' ✅ enabled')
+def main():
+ parser = argparse.ArgumentParser(
+ description='Re-enable GitHub Actions for all repos in a packages.txt'
+ ' files.')
+ parser.add_argument(
+ '--force-run',
+ help='Run workflow even it is already enabled.',
+ action='store_true')
+
+ args = parser.parse_args()
+
+ for repo in ALL_REPOS:
+ print(repo)
+ wfs = call(
+ 'gh', 'workflow', 'list', '--all', '-R', f'{ORG}/{repo}',
+ capture_output=True).stdout
+ test_line = [x for x in wfs.splitlines() if x.startswith('test')][0]
+ if 'disabled_inactivity' not in test_line:
+ print(' ☑️ already enabled')
+ if args.force_run:
+ run_workflow(base_url, ORG, repo)
+ continue
+ test_id = test_line.split()[-1]
+ call('gh', 'workflow', 'enable', test_id, '-R', f'{ORG}/{repo}')
+ if run_workflow(base_url, ORG, repo):
+ print(' ✅ enabled')
diff --git a/config/set_branch_protection_rules.py b/src/zope/meta/set_branch_protection_rules.py
similarity index 89%
rename from config/set_branch_protection_rules.py
rename to src/zope/meta/set_branch_protection_rules.py
index 4402c780..fdf2e76a 100644
--- a/config/set_branch_protection_rules.py
+++ b/src/zope/meta/set_branch_protection_rules.py
@@ -1,22 +1,25 @@
#!/usr/bin/env python3
-from shared.call import abort
-from shared.call import call
-from shared.packages import ALL_REPOS
-from shared.packages import MANYLINUX_AARCH64
-from shared.packages import MANYLINUX_I686
-from shared.packages import MANYLINUX_PYTHON_VERSION
-from shared.packages import MANYLINUX_X86_64
-from shared.packages import NEWEST_PYTHON_VERSION
-from shared.packages import OLDEST_PYTHON_VERSION
-from shared.packages import ORG
-from shared.packages import PYPY_VERSION
import argparse
import json
import os
import pathlib
-import requests
import tempfile
-import tomllib
+from typing import Optional
+
+import requests
+import tomlkit
+
+from .shared.call import abort
+from .shared.call import call
+from .shared.packages import ALL_REPOS
+from .shared.packages import MANYLINUX_AARCH64
+from .shared.packages import MANYLINUX_I686
+from .shared.packages import MANYLINUX_PYTHON_VERSION
+from .shared.packages import MANYLINUX_X86_64
+from .shared.packages import NEWEST_PYTHON_VERSION
+from .shared.packages import OLDEST_PYTHON_VERSION
+from .shared.packages import ORG
+from .shared.packages import PYPY_VERSION
BASE_URL = f'https://raw.githubusercontent.com/{ORG}'
@@ -39,7 +42,8 @@ def _call_gh(
allowed_return_codes=allowed_return_codes)
-def set_branch_protection(repo: str, meta_path: pathlib.Path | None) -> bool:
+def set_branch_protection(
+ repo: str, meta_path: Optional[pathlib.Path] = None) -> bool:
result = _call_gh(
'GET', 'protection/required_pull_request_reviews', repo,
allowed_return_codes=(0, 1))
@@ -60,10 +64,10 @@ def set_branch_protection(repo: str, meta_path: pathlib.Path | None) -> bool:
if meta_path is None:
response = requests.get(
f'{BASE_URL}/{repo}/{DEFAULT_BRANCH}/.meta.toml', timeout=30)
- meta_toml = tomllib.loads(response.text)
+ meta_toml = tomlkit.loads(response.text)
else:
with open(meta_path) as f:
- meta_toml = tomllib.loads(f.read())
+ meta_toml = tomlkit.load(f)
template = meta_toml['meta']['template']
with_docs = meta_toml['python'].get('with-docs', False)
with_pypy = meta_toml['python']['with-pypy']
@@ -147,7 +151,7 @@ def set_branch_protection(repo: str, meta_path: pathlib.Path | None) -> bool:
return True
-if __name__ == '__main__':
+def main():
parser = argparse.ArgumentParser(
description='Set the branch protection rules for all known packages.\n'
'Prerequsites: `gh auth login`.')
diff --git a/config/shared/__init__.py b/src/zope/meta/shared/__init__.py
similarity index 100%
rename from config/shared/__init__.py
rename to src/zope/meta/shared/__init__.py
diff --git a/config/shared/call.py b/src/zope/meta/shared/call.py
similarity index 100%
rename from config/shared/call.py
rename to src/zope/meta/shared/call.py
diff --git a/config/shared/git.py b/src/zope/meta/shared/git.py
similarity index 99%
rename from config/shared/git.py
rename to src/zope/meta/shared/git.py
index 88f8c1d0..f388fb48 100644
--- a/config/shared/git.py
+++ b/src/zope/meta/shared/git.py
@@ -10,9 +10,10 @@
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
+import pathlib
+
from .call import call
from .path import change_dir
-import pathlib
def get_commit_id():
diff --git a/config/shared/packages.py b/src/zope/meta/shared/packages.py
similarity index 98%
rename from config/shared/packages.py
rename to src/zope/meta/shared/packages.py
index b55cbba6..7b072d41 100644
--- a/config/shared/packages.py
+++ b/src/zope/meta/shared/packages.py
@@ -10,11 +10,12 @@
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
-from packaging.version import parse as parse_version
import configparser
import itertools
import pathlib
+from packaging.version import parse as parse_version
+
TYPES = ['buildout-recipe', 'c-code', 'pure-python', 'zope-product', 'toolkit']
ORG = 'zopefoundation'
@@ -159,7 +160,7 @@ def supported_python_versions(short_version=False):
minor_versions = []
oldest_python = parse_version(OLDEST_PYTHON_VERSION)
newest_python = parse_version(NEWEST_PYTHON_VERSION)
- for minor in range(oldest_python.minor, newest_python.minor+1):
+ for minor in range(oldest_python.minor, newest_python.minor + 1):
minor_versions.append(minor)
supported = [f'{oldest_python.major}.{minor}' for minor in minor_versions]
diff --git a/config/shared/path.py b/src/zope/meta/shared/path.py
similarity index 100%
rename from config/shared/path.py
rename to src/zope/meta/shared/path.py
diff --git a/src/zope/meta/tests/__init__.py b/src/zope/meta/tests/__init__.py
new file mode 100644
index 00000000..5bb534f7
--- /dev/null
+++ b/src/zope/meta/tests/__init__.py
@@ -0,0 +1 @@
+# package
diff --git a/src/zope/meta/tests/test_config_package.py b/src/zope/meta/tests/test_config_package.py
new file mode 100644
index 00000000..29f0b72d
--- /dev/null
+++ b/src/zope/meta/tests/test_config_package.py
@@ -0,0 +1,24 @@
+##############################################################################
+#
+# Copyright (c) 2024 Zope Foundation and Contributors.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+
+import unittest
+
+
+class ConfigPackageTests(unittest.TestCase):
+
+ def test_prepend_space(self):
+ from zope.meta.config_package import prepend_space
+
+ self.assertIsNone(prepend_space(None))
+ self.assertEqual('', prepend_space(''))
+ self.assertEqual(' foobar', prepend_space('foobar'))
diff --git a/config/toolkit/packages.txt b/src/zope/meta/toolkit/packages.txt
similarity index 100%
rename from config/toolkit/packages.txt
rename to src/zope/meta/toolkit/packages.txt
diff --git a/config/toolkit/tox.ini.j2 b/src/zope/meta/toolkit/tox.ini.j2
similarity index 100%
rename from config/toolkit/tox.ini.j2
rename to src/zope/meta/toolkit/tox.ini.j2
diff --git a/src/zope/meta/update_python_support.py b/src/zope/meta/update_python_support.py
new file mode 100644
index 00000000..4aa86abc
--- /dev/null
+++ b/src/zope/meta/update_python_support.py
@@ -0,0 +1,194 @@
+#!/usr/bin/env python3
+##############################################################################
+#
+# Copyright (c) 2022 Zope Foundation and Contributors.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+import argparse
+import collections
+import configparser
+import os
+import pathlib
+import shutil
+import sys
+
+import tomlkit
+
+from .shared.call import call
+from .shared.call import wait_for_accept
+from .shared.git import get_branch_name
+from .shared.git import git_branch
+from .shared.packages import OLDEST_PYTHON_VERSION
+from .shared.packages import supported_python_versions
+from .shared.path import change_dir
+
+
+def get_tox_ini_python_versions(path):
+ config = configparser.ConfigParser()
+ config.read(path)
+ envs = config['tox']['envlist'].split()
+ versions = [
+ env.replace('py3', '3.') for env in envs
+ if env.startswith('py') and env != 'pypy3'
+ ]
+ return versions
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ description='Update Python versions of a package to currently '
+ 'supported ones.')
+ parser.add_argument('path',
+ type=pathlib.Path,
+ help='path to the repository to be configured')
+ parser.add_argument(
+ '--branch',
+ dest='branch_name',
+ default=None,
+ help='Define a git branch name to be used for the changes. If not'
+ ' given it is constructed automatically and includes the configuration'
+ ' type')
+ parser.add_argument(
+ '--no-commit',
+ dest='commit',
+ action='store_false',
+ default=True,
+ help='Don\'t "git commit" changes made by this script.')
+ parser.add_argument(
+ '--interactive',
+ dest='interactive',
+ action='store_true',
+ default=False,
+ help='Run interactively: Scripts will prompt for input. Implies '
+ '--no-commit, changes will not be committed and pushed automatically.')
+
+ args = parser.parse_args()
+ path = args.path.absolute()
+
+ if not (path / '.git').exists():
+ raise ValueError(
+ '`path` does not point to a git clone of a repository!')
+ if not (path / '.meta.toml').exists():
+ raise ValueError('The repository `path` points to has no .meta.toml!')
+
+ with change_dir(path) as cwd_str:
+ cwd = pathlib.Path(cwd_str)
+ bin_dir = cwd / 'bin'
+ with open('.meta.toml', 'rb') as meta_f:
+ meta_toml = collections.defaultdict(dict, **tomlkit.load(meta_f))
+ config_type = meta_toml['meta']['template']
+ 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))
+
+ non_interactive_params = []
+ python_versions_args = []
+ if not args.interactive and args.commit:
+ non_interactive_params = ['--no-input']
+ else:
+ args.commit = False
+
+ if no_longer_supported or not_yet_supported:
+ call(bin_dir / 'bumpversion', '--feature', *non_interactive_params)
+ else:
+ print('No changes required.')
+ 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)
+ 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())]
+
+ if no_longer_supported or not_yet_supported:
+ call(bin_dir / 'check-python-versions', '--only=setup.py',
+ *python_versions_args)
+ print('Look through .meta.toml to see if it needs changes.')
+ call(os.environ['EDITOR'], '.meta.toml')
+
+ config_package_args = [
+ sys.executable,
+ 'config-package.py',
+ path,
+ f'--branch={branch_name}',
+ '--no-push',
+ ]
+ if not args.commit:
+ 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'
+ call('find', src, '-name', '*.py', '-exec', bin_dir / 'pyupgrade',
+ '--py3-plus', py_ver_plus, '{}', ';')
+ call(bin_dir / 'pyupgrade',
+ '--py3-plus',
+ py_ver_plus,
+ 'setup.py',
+ allowed_return_codes=(0, 1))
+
+ excludes = (
+ '--exclude-dir',
+ '__pycache__',
+ '--exclude-dir',
+ '*.egg-info',
+ '--exclude',
+ '*.pyc',
+ '--exclude',
+ '*.so')
+ print('Replace any remaining code that might'
+ ' support legacy Python:')
+ call(
+ 'egrep',
+ '-rn',
+ f'{"|".join(no_longer_supported)}|sys.version|PY3|Py3|Python 3'
+ '|__unicode__|ImportError',
+ src,
+ *excludes,
+ allowed_return_codes=(
+ 0,
+ 1))
+ wait_for_accept()
+ tox_path = shutil.which('tox') or (cwd / 'bin' / 'tox')
+ call(tox_path, '-p', 'auto')
+ if args.commit:
+ print('Adding, committing and pushing all changes ...')
+ call('git', 'add', '.')
+ call('git', 'commit', '-m', 'Update Python version support.')
+ call('git', 'push', '--set-upstream', 'origin', branch_name)
+ if updating:
+ print('Updated the previously created PR.')
+ else:
+ print(
+ 'Are you logged in via `gh auth login` to'
+ ' create a PR? (y/N)?', end=' ')
+ if input().lower() == 'y':
+ call('gh', 'pr', 'create', '--fill', '--title',
+ 'Update Python version support.')
+ else:
+ print('If everything went fine up to here:')
+ print('Create a PR, using the URL shown above.')
+ else:
+ print('Applied all changes. Please check and commit manually.')
diff --git a/config/zope-product/packages.txt b/src/zope/meta/zope-product/packages.txt
similarity index 100%
rename from config/zope-product/packages.txt
rename to src/zope/meta/zope-product/packages.txt
diff --git a/config/zope-product/tox.ini.j2 b/src/zope/meta/zope-product/tox.ini.j2
similarity index 100%
rename from config/zope-product/tox.ini.j2
rename to src/zope/meta/zope-product/tox.ini.j2
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 00000000..fb5add54
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,80 @@
+# Generated from:
+# https://github.com/zopefoundation/meta/tree/master/config/pure-python
+[tox]
+minversion = 3.18
+envlist =
+ release-check
+ lint
+ py38
+ py39
+ py310
+ py311
+ py312
+ py313
+ docs
+ coverage
+
+[testenv]
+usedevelop = true
+package = wheel
+wheel_build_env = .pkg
+deps =
+ setuptools <74
+commands =
+ zope-testrunner --test-path=src {posargs:-vc}
+extras =
+ test
+
+[testenv:setuptools-latest]
+basepython = python3
+deps =
+ git+https://github.com/pypa/setuptools.git\#egg=setuptools
+
+[testenv:release-check]
+description = ensure that the distribution is ready to release
+basepython = python3
+skip_install = true
+deps =
+ setuptools <74
+ twine
+ build
+ check-manifest
+ check-python-versions >= 0.20.0
+ wheel
+commands_pre =
+commands =
+ check-manifest
+ check-python-versions --only setup.py,tox.ini,.github/workflows/tests.yml
+ python -m build --sdist --no-isolation
+ twine check dist/*
+
+[testenv:lint]
+description = This env runs all linters configured in .pre-commit-config.yaml
+basepython = python3
+skip_install = true
+deps =
+ pre-commit
+commands_pre =
+commands =
+ pre-commit run --all-files --show-diff-on-failure
+
+[testenv:docs]
+basepython = python3
+skip_install = false
+extras =
+ docs
+commands_pre =
+commands =
+ sphinx-build -b html -d docs/_build/doctrees docs docs/_build/html
+
+[testenv:coverage]
+basepython = python3
+allowlist_externals =
+ mkdir
+deps =
+ coverage[toml]
+commands =
+ mkdir -p {toxinidir}/parts/htmlcov
+ coverage run -m zope.testrunner --test-path=src {posargs:-vc}
+ coverage html
+ coverage report