Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to run bin/update-python-support in CI. #308

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ Change log
- Add script ``bin/switch-to-pep420`` to convert a package from the old
namespace package layout to the new PEP 420 native layout.

- Add ability to run ``bin/update-python-support`` in CI.


1.1 (2025-01-29)
----------------
Expand Down
55 changes: 35 additions & 20 deletions src/zope/meta/config_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from packaging.version import InvalidVersion
from packaging.version import parse as parse_version

from .set_branch_protection_rules import get_package_name
from .set_branch_protection_rules import set_branch_protection
from .shared.call import abort
from .shared.call import call
Expand Down Expand Up @@ -156,6 +157,19 @@ def handle_command_line_arguments():
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-admin',
dest='admin',
action='store_false',
default=True,
help='Do not try to do steps that require GitHub admin rights.')
parser.add_argument(
'--started-from-auto-update',
dest='started_from_auto_update',
action='store_true',
default=False,
help=argparse.SUPPRESS # no to be used by humans
)

args = parser.parse_args()
return args
Expand All @@ -174,7 +188,7 @@ def prepend_space(text):
class PackageConfiguration:
add_manylinux = False

def __init__(self, args):
def __init__(self, args, in_checkout=False):
self.args = args
self.path = args.path.absolute()
self.meta_cfg = {}
Expand All @@ -186,7 +200,8 @@ def __init__(self, args):

self.meta_cfg = self._read_meta_configuration()
self.meta_cfg['meta']['template'] = self.config_type
self.meta_cfg['meta']['commit-id'] = get_commit_id()
commit_id = get_commit_id() if in_checkout else 'auto-update'
self.meta_cfg['meta']['commit-id'] = commit_id

def _read_meta_configuration(self):
"""Read and update meta configuration"""
Expand Down Expand Up @@ -717,9 +732,10 @@ def configure(self):
meta_f.write('\n')
tomlkit.dump(meta_cfg, meta_f)

tox_path = shutil.which('tox') or (
pathlib.Path(cwd) / 'bin' / 'tox')
call(tox_path, '-p', 'auto')
if not self.args.started_from_auto_update:
tox_path = shutil.which('tox') or (
pathlib.Path(cwd) / 'bin' / 'tox')
call(tox_path, '-p', 'auto')

updating = git_branch(self.branch_name)

Expand Down Expand Up @@ -750,20 +766,18 @@ def configure(self):
if self.args.push:
call('git', 'push', '--set-upstream',
'origin', self.branch_name)
print()
print('If you are an admin and are logged in via `gh auth login`')
print('update branch protection rules? (y/N)?', end=' ')
if input().lower() == 'y':
remote_url = call(
'git', 'config', '--get', 'remote.origin.url',
capture_output=True).stdout.strip()
package_name = remote_url.rsplit('/')[-1].removesuffix('.git')
success = set_branch_protection(
package_name, self.path / '.meta.toml')
if success:
print('Successfully updated branch protection rules.')
else:
abort(-1)
if self.args.admin:
print()
print('If you are an admin and are logged in via `gh auth'
' login`')
print('update branch protection rules? (y/N)?', end=' ')
if input().lower() == 'y':
success = set_branch_protection(
get_package_name(), self.path / '.meta.toml')
if success:
print('Successfully updated branch protection rules.')
else:
abort(-1)
print()
print('If everything went fine up to here:')
if updating:
Expand All @@ -774,6 +788,7 @@ def configure(self):

def main():
args = handle_command_line_arguments()
in_checkout = (pathlib.Path(__file__).absolute().parent / '.git').exists()

package = PackageConfiguration(args)
package = PackageConfiguration(args, in_checkout)
package.configure()
7 changes: 7 additions & 0 deletions src/zope/meta/set_branch_protection_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ def _call_gh(
allowed_return_codes=allowed_return_codes)


def get_package_name():
remote_url = call(
'git', 'config', '--get', 'remote.origin.url',
capture_output=True).stdout.strip()
return remote_url.rsplit('/')[-1].removesuffix('.git')


def set_branch_protection(
repo: str, meta_path: Optional[pathlib.Path] = None) -> bool:
result = _call_gh(
Expand Down
8 changes: 5 additions & 3 deletions src/zope/meta/shared/call.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,14 @@ def wait_for_accept():
input()


def call(*args, capture_output=False, cwd=None, allowed_return_codes=(0, )):
def call(*args, capture_output=False, cwd=None, allowed_return_codes=(0, ),
input: str | None = None):
"""Call `args` as a subprocess.

If it fails exit the process.
"""
result = subprocess.run(
args, capture_output=capture_output, text=True, cwd=cwd)
args, capture_output=capture_output, text=True, cwd=cwd, input=input)
if result.returncode not in allowed_return_codes:
if capture_output:
abort_text = textwrap.dedent(f'''
Expand All @@ -44,5 +45,6 @@ def call(*args, capture_output=False, cwd=None, allowed_return_codes=(0, )):
stdout: {result.stdout}''')
else:
abort_text = result.returncode
abort(abort_text)
print(abort_text)
abort(result.returncode)
return result
108 changes: 71 additions & 37 deletions src/zope/meta/update_python_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,17 @@ def main():
default=False,
help='Run interactively: Scripts will prompt for input. Implies '
'--no-commit, changes will not be committed and pushed automatically.')
parser.add_argument(
'--auto-update',
dest='auto_update',
action='store_true',
default=False,
help='Run without any questions asked. Only to be used by CI.')

args = parser.parse_args()
path = args.path.absolute()
zope_meta_dir = pathlib.Path(sys.argv[0]).absolute().parent.parent
bin_dir = zope_meta_dir / 'bin'

if not (path / '.git').exists():
raise ValueError(
Expand All @@ -79,15 +87,11 @@ def main():
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']
oldest_python_version = meta_toml['python'].get('oldest-python',
OLDEST_PYTHON_VERSION)
branch_name = get_branch_name(args.branch_name, config_type)
updating = git_branch(branch_name)

current_python_versions = get_tox_ini_python_versions('tox.ini')
no_longer_supported = (
Expand All @@ -107,6 +111,8 @@ def main():
args.commit = False

if no_longer_supported or not_yet_supported:
branch_name = get_branch_name(args.branch_name, config_type)
updating = git_branch(branch_name)
call(bin_dir / 'bumpversion', '--feature', *non_interactive_params)
else:
print('No changes required.')
Expand All @@ -132,20 +138,37 @@ def main():
)

if no_longer_supported or not_yet_supported:
if args.interactive:
input = None
else:
input = 'Y\nY\n'
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')
*python_versions_args, input=input)
if not args.auto_update:
print('Look through .meta.toml to see if it needs changes.')
call(os.environ['EDITOR'], '.meta.toml')

config_package_args = [
bin_dir / 'config-package',
path,
f'--branch={branch_name}',
'--no-push',
]
if args.auto_update:
# Admin commands require more permissions than a workflow might
# be able to get:
config_package_args.extend(
['--no-admin', '--started-from-auto-update'])
if not args.commit:
config_package_args.append('--no-commit')

call(*config_package_args, cwd=cwd_str)
# GitHub does not allow this inside the workflow, at least I did
# not find a way to do it:
# if args.auto_update:
# set_branch_protection(
# get_package_name(), path / '.meta.toml')

src = path.resolve() / 'src'
py_ver_plus = f'--py{oldest_python_version.replace(".", "")}-plus'
call('find', src, '-name', '*.py', '-exec', bin_dir / 'pyupgrade',
Expand All @@ -156,30 +179,34 @@ def main():
'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 not args.auto_update:
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()

if not args.auto_update:
tox_path = shutil.which('tox') or (bin_dir / 'tox')
call(tox_path, '-p', 'auto')

if args.commit:
print('Adding, committing and pushing all changes ...')
call('git', 'add', '.')
Expand All @@ -188,14 +215,21 @@ def main():
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':
create_pr = False
if args.auto_update:
create_pr = True
else:
print(
'Are you logged in via `gh auth login` to'
' create a PR? (y/N)?', end=' ')
if input().lower() == 'y':
create_pr = True
if create_pr:
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.')
if not args.auto_update:
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.')
Loading