Skip to content

Commit 51191c2

Browse files
authored
Merge pull request #297 from plone/pep-420-native-namespace
Script: switch to PEP 420 native namespace
2 parents 5ea867a + 3ef87b6 commit 51191c2

File tree

3 files changed

+200
-0
lines changed

3 files changed

+200
-0
lines changed

news/3982.feature.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add and adapt switch-to-pep420 script from zope meta.

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ dependencies = [
4040
"tomlkit",
4141
"tox",
4242
"validate-pyproject[all]",
43+
"zest.releaser",
4344
]
4445

4546
[project.urls]
@@ -52,6 +53,7 @@ Documentation = "https://6.docs.plone.org/developer-guide/standardize-python-pro
5253
config-package = "plone.meta.config_package:main"
5354
multi-call = "plone.meta.multi_call:main"
5455
re-enable-actions = "plone.meta.re_enable_actions:main"
56+
switch-to-pep420 = "plone.meta.pep_420:main"
5557

5658
[tool.towncrier]
5759
directory = "news/"

src/plone/meta/pep_420.py

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
#!/usr/bin/env python3
2+
##############################################################################
3+
#
4+
# Copyright (c) 2025 Zope Foundation and Contributors.
5+
#
6+
# This software is subject to the provisions of the Zope Public License,
7+
# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
8+
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
9+
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
10+
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
11+
# FOR A PARTICULAR PURPOSE.
12+
#
13+
##############################################################################
14+
from .shared.call import call
15+
from .shared.git import git_branch
16+
from .shared.path import change_dir
17+
18+
import argparse
19+
import pathlib
20+
import shutil
21+
22+
23+
def main():
24+
parser = argparse.ArgumentParser(
25+
description="Update a repository to PEP 420 native namespace."
26+
)
27+
parser.add_argument(
28+
"path", type=pathlib.Path, help="path to the repository to be updated"
29+
)
30+
parser.add_argument(
31+
"--branch",
32+
dest="branch_name",
33+
default=None,
34+
help="Define a git branch name to be used for the changes. If not"
35+
" given it is constructed automatically and includes the configuration"
36+
" type",
37+
)
38+
parser.add_argument(
39+
"--no-breaking",
40+
dest="breaking",
41+
action="store_false",
42+
default=True,
43+
help="Don't bump for breaking change. Use this if release is already alpha.",
44+
)
45+
parser.add_argument(
46+
"--no-commit",
47+
dest="commit",
48+
action="store_false",
49+
default=True,
50+
help='Don\'t "git commit" changes made by this script.',
51+
)
52+
parser.add_argument(
53+
"--push",
54+
dest="push",
55+
action="store_true",
56+
default=False,
57+
help="Push changes directly.",
58+
)
59+
parser.add_argument(
60+
"--interactive",
61+
dest="interactive",
62+
action="store_true",
63+
default=False,
64+
help="Run interactively: Scripts will prompt for input. Implies "
65+
"--no-commit, changes will not be committed and pushed automatically.",
66+
)
67+
parser.add_argument(
68+
"--no-tests",
69+
dest="run_tests",
70+
action="store_false",
71+
default=True,
72+
help="Skip running unit tests.",
73+
)
74+
75+
args = parser.parse_args()
76+
path = args.path.absolute()
77+
78+
if not (path / ".git").exists():
79+
raise ValueError("`path` does not point to a git clone of a repository!")
80+
if not (path / ".meta.toml").exists():
81+
raise ValueError("The repository `path` points to has no .meta.toml!")
82+
83+
with change_dir(path) as cwd_str:
84+
cwd = pathlib.Path(cwd_str)
85+
bin_dir = cwd / "venv" / "bin"
86+
branch_name = args.branch_name or "pep-420-native-namespace"
87+
updating = git_branch(branch_name)
88+
89+
non_interactive_params = []
90+
if not args.interactive and args.commit:
91+
non_interactive_params = ["--no-input"]
92+
else:
93+
args.commit = False
94+
95+
if args.breaking:
96+
call(bin_dir / "bumpversion", "--breaking", *non_interactive_params)
97+
(path / "news" / "3928.breaking").write_text(
98+
"Replace ``pkg_resources`` namespace with PEP 420 native namespace.\n"
99+
"Support only Plone 6.2 and Python 3.10+.\n"
100+
)
101+
102+
setup_py = []
103+
setup_text = (path / "setup.py").read_text()
104+
has_62_classifier = "Framework :: Plone :: 6.2" in setup_text
105+
for line in setup_text.splitlines():
106+
if "from setuptools import find_packages" in line:
107+
continue
108+
elif "namespace_packages" in line:
109+
continue
110+
elif "packages=" in line:
111+
continue
112+
elif "package_dir=" in line:
113+
continue
114+
elif "zope.testrunner" in line:
115+
setup_py.append(
116+
line.replace("zope.testrunner", "zope.testrunner >= 6.4")
117+
)
118+
elif "Framework :: Plone :: 6.0" in line:
119+
continue
120+
elif "Framework :: Plone :: 6.1" in line:
121+
continue
122+
elif "Programming Language :: Python :: 3.8" in line:
123+
continue
124+
elif "Programming Language :: Python :: 3.9" in line:
125+
continue
126+
elif 'python_requires=">=3.8"' in line:
127+
setup_py.append(
128+
line.replace('python_requires=">=3.8"', 'python_requires=">=3.10"')
129+
)
130+
elif 'python_requires=">=3.9"' in line:
131+
setup_py.append(
132+
line.replace('python_requires=">=3.9"', 'python_requires=">=3.10"')
133+
)
134+
else:
135+
setup_py.append(line)
136+
# One extra check after the line has been added.
137+
if (
138+
not has_62_classifier
139+
and "Framework :: Plone" in line
140+
and "Framework :: Plone ::" not in line
141+
):
142+
setup_py.append(
143+
line.replace("Framework :: Plone", "Framework :: Plone :: 6.2")
144+
)
145+
146+
(path / "setup.py").write_text("\n".join(setup_py) + "\n")
147+
148+
for src_dir_cont in (path / "src").iterdir():
149+
if not src_dir_cont.is_dir():
150+
continue
151+
pkg_init = src_dir_cont / "__init__.py"
152+
if pkg_init.exists():
153+
pkg_init.unlink()
154+
for pkg_dir_cont in src_dir_cont.iterdir():
155+
if not pkg_dir_cont.is_dir():
156+
continue
157+
sub_pkg_init = pkg_dir_cont / "__init__.py"
158+
if sub_pkg_init.exists():
159+
if "pkg_resources" in sub_pkg_init.read_text():
160+
sub_pkg_init.unlink()
161+
162+
if args.commit:
163+
print("Adding all changes ...")
164+
call("git", "add", ".")
165+
166+
if args.run_tests:
167+
tox_path = shutil.which("tox") or (cwd / "venv" / "bin" / "tox")
168+
call(tox_path, "-p", "auto")
169+
170+
if args.commit:
171+
print("Committing all changes ...")
172+
call("git", "commit", "-m", "Switch to PEP 420 native namespace.")
173+
if not args.push:
174+
print("All changes committed. Please check and push manually.")
175+
return
176+
call("git", "push", "--set-upstream", "origin", branch_name)
177+
if updating:
178+
print("Updated the previously created PR.")
179+
else:
180+
print(
181+
"Are you logged in via `gh auth login` to create a PR? (y/N)?",
182+
end=" ",
183+
)
184+
if input().lower() == "y":
185+
call(
186+
"gh",
187+
"pr",
188+
"create",
189+
"--fill",
190+
"--title",
191+
"Switch to PEP 420 native namespace.",
192+
)
193+
else:
194+
print("If everything went fine up to here:")
195+
print("Create a PR, using the URL shown above.")
196+
else:
197+
print("Applied all changes. Please check and commit manually.")

0 commit comments

Comments
 (0)