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