plone.meta 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plone/__init__.py +1 -0
- plone/meta/__init__.py +0 -0
- plone/meta/config_package.py +639 -0
- plone/meta/default/changelog_template.jinja +15 -0
- plone/meta/default/dependabot.yml +8 -0
- plone/meta/default/editorconfig.j2 +53 -0
- plone/meta/default/flake8.j2 +19 -0
- plone/meta/default/gitignore.j2 +55 -0
- plone/meta/default/gitlab-ci.yml.j2 +130 -0
- plone/meta/default/meta.yml.j2 +80 -0
- plone/meta/default/packages.txt +99 -0
- plone/meta/default/pre-commit-config.yaml.j2 +91 -0
- plone/meta/default/pyproject.toml.j2 +184 -0
- plone/meta/default/tox.ini.j2 +328 -0
- plone/meta/multi_call.py +52 -0
- plone/meta/re_enable_actions.py +79 -0
- plone/meta/shared/__init__.py +0 -0
- plone/meta/shared/call.py +32 -0
- plone/meta/shared/git.py +50 -0
- plone/meta/shared/packages.py +9 -0
- plone/meta/shared/path.py +39 -0
- plone/meta/shared/utils.py +36 -0
- plone.meta-2.0.0.dist-info/LICENSE +339 -0
- plone.meta-2.0.0.dist-info/METADATA +1039 -0
- plone.meta-2.0.0.dist-info/RECORD +28 -0
- plone.meta-2.0.0.dist-info/WHEEL +5 -0
- plone.meta-2.0.0.dist-info/entry_points.txt +4 -0
- plone.meta-2.0.0.dist-info/top_level.txt +1 -0
plone/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__import__("pkg_resources").declare_namespace(__name__)
|
plone/meta/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
from .shared.call import call
|
|
2
|
+
from .shared.git import get_branch_name
|
|
3
|
+
from .shared.git import git_branch
|
|
4
|
+
from .shared.git import git_server_url
|
|
5
|
+
from .shared.path import change_dir
|
|
6
|
+
from functools import cached_property
|
|
7
|
+
from importlib.metadata import version
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import collections
|
|
11
|
+
import configparser
|
|
12
|
+
import editorconfig
|
|
13
|
+
import jinja2
|
|
14
|
+
import pathlib
|
|
15
|
+
import re
|
|
16
|
+
import shutil
|
|
17
|
+
import tomlkit
|
|
18
|
+
import validate_pyproject
|
|
19
|
+
import yaml
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
META_HINT = """\
|
|
23
|
+
# Generated from:
|
|
24
|
+
# https://github.com/plone/meta/tree/main/src/plone/meta/{config_type}
|
|
25
|
+
# See the inline comments on how to expand/tweak this configuration file"""
|
|
26
|
+
META_HINT_MARKDOWN = """\
|
|
27
|
+
<!--
|
|
28
|
+
Generated from:
|
|
29
|
+
https://github.com/plone/meta/tree/main/src/plone/meta/{config_type}
|
|
30
|
+
See the inline comments on how to expand/tweak this configuration file
|
|
31
|
+
--> """
|
|
32
|
+
DEFAULT = object()
|
|
33
|
+
|
|
34
|
+
MXDEV_CONSTRAINTS = "constraints-mxdev.txt"
|
|
35
|
+
PLONE_CONSTRAINTS = "https://dist.plone.org/release/6.0-dev/constraints.txt"
|
|
36
|
+
|
|
37
|
+
DOCKER_IMAGE = "python:3.11-bullseye"
|
|
38
|
+
|
|
39
|
+
# Rather than pointing configured repositories to `plone.meta`'s `main` branch
|
|
40
|
+
# to get their GHA workflows, point them to an ever evolving branch.
|
|
41
|
+
#
|
|
42
|
+
# This has a few benefits:
|
|
43
|
+
# - configured repositories do not point to outdated GHA workflows
|
|
44
|
+
# - `plone.meta` can do breaking changes on GHA workflows and roll them gradually
|
|
45
|
+
GHA_DEFAULT_REF = "2.x"
|
|
46
|
+
|
|
47
|
+
GHA_DEFAULT_JOBS = [
|
|
48
|
+
"qa",
|
|
49
|
+
"test",
|
|
50
|
+
"coverage",
|
|
51
|
+
"dependencies",
|
|
52
|
+
"release_ready",
|
|
53
|
+
"circular",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
GITLAB_DEFAULT_JOBS = [
|
|
57
|
+
"lint",
|
|
58
|
+
"release-ready",
|
|
59
|
+
"dependencies",
|
|
60
|
+
"circular-dependencies",
|
|
61
|
+
"testing",
|
|
62
|
+
"coverage",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def handle_command_line_arguments():
|
|
67
|
+
"""Parse command line options"""
|
|
68
|
+
parser = argparse.ArgumentParser(description="Use configuration for a package.")
|
|
69
|
+
parser.add_argument(
|
|
70
|
+
"path", type=pathlib.Path, help="path to the repository to be configured"
|
|
71
|
+
)
|
|
72
|
+
parser.add_argument(
|
|
73
|
+
"--commit-msg",
|
|
74
|
+
dest="commit_msg",
|
|
75
|
+
metavar="MSG",
|
|
76
|
+
help="Use MSG as commit message instead of an artificial one.",
|
|
77
|
+
)
|
|
78
|
+
parser.add_argument(
|
|
79
|
+
"--no-commit",
|
|
80
|
+
dest="commit",
|
|
81
|
+
action="store_false",
|
|
82
|
+
default=True,
|
|
83
|
+
help="Prevent automatic committing of changes.",
|
|
84
|
+
)
|
|
85
|
+
parser.add_argument(
|
|
86
|
+
"--push",
|
|
87
|
+
dest="push",
|
|
88
|
+
action="store_true",
|
|
89
|
+
default=False,
|
|
90
|
+
help="Push changes directly.",
|
|
91
|
+
)
|
|
92
|
+
parser.add_argument(
|
|
93
|
+
"-t",
|
|
94
|
+
"--type",
|
|
95
|
+
choices=[
|
|
96
|
+
"default",
|
|
97
|
+
],
|
|
98
|
+
default="default",
|
|
99
|
+
dest="type",
|
|
100
|
+
help="type of the configuration to be used, see README.md. "
|
|
101
|
+
"Only required when running on a repository for the first time.",
|
|
102
|
+
)
|
|
103
|
+
parser.add_argument(
|
|
104
|
+
"--tox",
|
|
105
|
+
dest="run_tox",
|
|
106
|
+
action="store_true",
|
|
107
|
+
default=False,
|
|
108
|
+
help="Whether to run tox after configuring the repository.",
|
|
109
|
+
)
|
|
110
|
+
parser.add_argument(
|
|
111
|
+
"--branch",
|
|
112
|
+
dest="branch_name",
|
|
113
|
+
default=None,
|
|
114
|
+
help="Define a git branch name to be used for the changes. "
|
|
115
|
+
"If not given it is constructed automatically and includes "
|
|
116
|
+
'the configuration type. Use "current" to update the current branch.',
|
|
117
|
+
)
|
|
118
|
+
parser.add_argument(
|
|
119
|
+
"--track",
|
|
120
|
+
dest="track_package",
|
|
121
|
+
action="store_true",
|
|
122
|
+
default=False,
|
|
123
|
+
help="Whether to add the package being configured in packages.txt.",
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
args = parser.parse_args()
|
|
127
|
+
return args
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class PackageConfiguration:
|
|
131
|
+
|
|
132
|
+
def __init__(self, args):
|
|
133
|
+
self.args = args
|
|
134
|
+
self.path = args.path.absolute()
|
|
135
|
+
self.meta_cfg = {}
|
|
136
|
+
|
|
137
|
+
if not (self.path / ".git").exists():
|
|
138
|
+
raise ValueError(
|
|
139
|
+
f"{self.path!r} does not point to a git clone of a repository!"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
self.meta_cfg = self._read_meta_configuration()
|
|
143
|
+
self.meta_cfg["meta"]["template"] = self.config_type
|
|
144
|
+
self.meta_cfg["meta"]["commit-id"] = self._get_version()
|
|
145
|
+
|
|
146
|
+
with change_dir(self.path):
|
|
147
|
+
server_url = git_server_url()
|
|
148
|
+
self.is_github = "github" in server_url
|
|
149
|
+
self.is_gitlab = "gitlab" in server_url
|
|
150
|
+
if not self.is_github and not self.is_gitlab:
|
|
151
|
+
self.print_warning(
|
|
152
|
+
"CI configuration",
|
|
153
|
+
"The repository is not hosted in github nor in gitlab, no CI configuration will be done!",
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def _get_version(self):
|
|
157
|
+
return version("plone.meta")
|
|
158
|
+
|
|
159
|
+
def _read_meta_configuration(self):
|
|
160
|
+
"""Read and update meta configuration"""
|
|
161
|
+
meta_toml_path = self.path / ".meta.toml"
|
|
162
|
+
if meta_toml_path.exists():
|
|
163
|
+
with open(meta_toml_path, "rb") as meta_f:
|
|
164
|
+
meta_cfg = tomlkit.load(meta_f)
|
|
165
|
+
meta_cfg = collections.defaultdict(dict, **meta_cfg)
|
|
166
|
+
else:
|
|
167
|
+
meta_cfg = collections.defaultdict(dict)
|
|
168
|
+
return meta_cfg
|
|
169
|
+
|
|
170
|
+
@cached_property
|
|
171
|
+
def config_type(self):
|
|
172
|
+
value = self.meta_cfg["meta"].get("template") or self.args.type
|
|
173
|
+
if value is None:
|
|
174
|
+
raise ValueError(
|
|
175
|
+
"Configuration type not set. Please use `--type` to select it."
|
|
176
|
+
)
|
|
177
|
+
return value
|
|
178
|
+
|
|
179
|
+
@cached_property
|
|
180
|
+
def config_type_path(self):
|
|
181
|
+
return pathlib.Path(__file__).parent / self.config_type
|
|
182
|
+
|
|
183
|
+
@cached_property
|
|
184
|
+
def default_path(self):
|
|
185
|
+
return pathlib.Path(__file__).parent / "default"
|
|
186
|
+
|
|
187
|
+
@cached_property
|
|
188
|
+
def jinja_env(self):
|
|
189
|
+
return jinja2.Environment(
|
|
190
|
+
loader=jinja2.FileSystemLoader([self.config_type_path, self.default_path]),
|
|
191
|
+
variable_start_string="%(",
|
|
192
|
+
variable_end_string=")s",
|
|
193
|
+
keep_trailing_newline=True,
|
|
194
|
+
trim_blocks=True,
|
|
195
|
+
lstrip_blocks=True,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
@cached_property
|
|
199
|
+
def branch_name(self):
|
|
200
|
+
return get_branch_name(self.args.branch_name, self.config_type)
|
|
201
|
+
|
|
202
|
+
def _add_project_to_config_type_list(self):
|
|
203
|
+
"""Add the current project to packages.txt if it is not there"""
|
|
204
|
+
with open(self.config_type_path / "packages.txt") as f:
|
|
205
|
+
known_packages = f.read().splitlines()
|
|
206
|
+
|
|
207
|
+
if self.path.name in known_packages:
|
|
208
|
+
print(
|
|
209
|
+
f"{self.path.name} is already configured "
|
|
210
|
+
"for this config type, updating."
|
|
211
|
+
)
|
|
212
|
+
return
|
|
213
|
+
print(f"{self.path.name} is not yet configured for this config type, adding.")
|
|
214
|
+
with open(self.config_type_path / "packages.txt", "a") as f:
|
|
215
|
+
f.write(f"{self.path.name}\n")
|
|
216
|
+
|
|
217
|
+
def cfg_option(self, section, name=None, default=DEFAULT):
|
|
218
|
+
"""Read a value from `self.meta_cfg`, default to `[]` if not existing."""
|
|
219
|
+
if default == DEFAULT:
|
|
220
|
+
default = []
|
|
221
|
+
if name is None:
|
|
222
|
+
# Get the entire section.
|
|
223
|
+
return self.meta_cfg[section]
|
|
224
|
+
return self.meta_cfg[section].get(name, default)
|
|
225
|
+
|
|
226
|
+
def _get_options_for(self, section, names):
|
|
227
|
+
"""Get all given named options from a given section"""
|
|
228
|
+
options = {}
|
|
229
|
+
for name in names:
|
|
230
|
+
options[name] = self.cfg_option(section, name, "")
|
|
231
|
+
return options
|
|
232
|
+
|
|
233
|
+
def _test_cfg(self):
|
|
234
|
+
"""Setup testing configuration."""
|
|
235
|
+
options = self._get_options_for("tox", ("test_runner", "test_path"))
|
|
236
|
+
path = options.get("test_path")
|
|
237
|
+
if not path:
|
|
238
|
+
if (self.path / "tests").exists():
|
|
239
|
+
path = "/tests"
|
|
240
|
+
elif (self.path / "src").exists():
|
|
241
|
+
path = "/src"
|
|
242
|
+
else:
|
|
243
|
+
path = ""
|
|
244
|
+
options["test_path"] = path
|
|
245
|
+
runner = options.get("test_runner", "zope.testrunner")
|
|
246
|
+
options["test_runner"] = runner
|
|
247
|
+
return options
|
|
248
|
+
|
|
249
|
+
def warn_on_setup_cfg(self):
|
|
250
|
+
"""Warn if setup.cfg has sections that we define in other files"""
|
|
251
|
+
setup_file = pathlib.Path(self.path / "setup.cfg")
|
|
252
|
+
if not setup_file.exists():
|
|
253
|
+
return
|
|
254
|
+
content = setup_file.read_text()
|
|
255
|
+
sections_outside = ("check-manifest", "flake8", "bdist_wheel")
|
|
256
|
+
prefix = "setup.cfg cleanup"
|
|
257
|
+
print()
|
|
258
|
+
for section_name in sections_outside:
|
|
259
|
+
if f"[{section_name}]" in content:
|
|
260
|
+
self.print_warning(prefix, f"please remove [{section_name}] section")
|
|
261
|
+
|
|
262
|
+
def print_warning(self, prefix, message):
|
|
263
|
+
print(f"*** {prefix}: {message}\n")
|
|
264
|
+
|
|
265
|
+
def editorconfig(self):
|
|
266
|
+
options = self._get_options_for("editorconfig", ("extra_lines",))
|
|
267
|
+
return self.copy_with_meta(
|
|
268
|
+
"editorconfig.j2", self.path / ".editorconfig", **options
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
def gitignore(self):
|
|
272
|
+
options = self._get_options_for("gitignore", ("extra_lines",))
|
|
273
|
+
return self.copy_with_meta("gitignore.j2", self.path / ".gitignore", **options)
|
|
274
|
+
|
|
275
|
+
def pre_commit_config(self):
|
|
276
|
+
options = self._get_options_for(
|
|
277
|
+
"pre_commit",
|
|
278
|
+
(
|
|
279
|
+
"zpretty_extra_lines",
|
|
280
|
+
"codespell_extra_lines",
|
|
281
|
+
"flake8_extra_lines",
|
|
282
|
+
"extra_lines",
|
|
283
|
+
"i18ndude_extra_lines",
|
|
284
|
+
),
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
return self.copy_with_meta(
|
|
288
|
+
"pre-commit-config.yaml.j2",
|
|
289
|
+
self.path / ".pre-commit-config.yaml",
|
|
290
|
+
**options,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
def pyproject_toml(self):
|
|
294
|
+
files = []
|
|
295
|
+
|
|
296
|
+
options = self._get_options_for(
|
|
297
|
+
"pyproject",
|
|
298
|
+
(
|
|
299
|
+
"codespell_ignores",
|
|
300
|
+
"codespell_skip",
|
|
301
|
+
"dependencies_ignores",
|
|
302
|
+
"dependencies_mappings",
|
|
303
|
+
"check_manifest_ignores",
|
|
304
|
+
"towncrier_issue_format",
|
|
305
|
+
"towncrier_extra_lines",
|
|
306
|
+
"isort_extra_lines",
|
|
307
|
+
"black_extra_lines",
|
|
308
|
+
"check_manifest_extra_lines",
|
|
309
|
+
"extra_lines",
|
|
310
|
+
),
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
options["changes_extension"] = "rst"
|
|
314
|
+
if (self.path / "CHANGES.md").exists():
|
|
315
|
+
options["changes_extension"] = "md"
|
|
316
|
+
|
|
317
|
+
options["news_folder_exists"] = False
|
|
318
|
+
news = self.path / "news"
|
|
319
|
+
if news.exists():
|
|
320
|
+
options["news_folder_exists"] = True
|
|
321
|
+
if options["changes_extension"] == "md":
|
|
322
|
+
destination = news / ".changelog_template.jinja"
|
|
323
|
+
shutil.copy(
|
|
324
|
+
self.config_type_path / "changelog_template.jinja", destination
|
|
325
|
+
)
|
|
326
|
+
files.append(destination)
|
|
327
|
+
else:
|
|
328
|
+
# only add the `.gitkeep` file if there is no jinja template
|
|
329
|
+
gitkeep = news / ".gitkeep"
|
|
330
|
+
gitkeep.touch(exist_ok=True)
|
|
331
|
+
|
|
332
|
+
else:
|
|
333
|
+
self.print_warning(
|
|
334
|
+
"towncrier",
|
|
335
|
+
"If you want to use Towncrier, you have to create a 'news/' folder manually.",
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
filename = self.copy_with_meta(
|
|
339
|
+
"pyproject.toml.j2",
|
|
340
|
+
**options,
|
|
341
|
+
)
|
|
342
|
+
files.append(filename)
|
|
343
|
+
return files
|
|
344
|
+
|
|
345
|
+
def tox(self):
|
|
346
|
+
options = self._get_options_for(
|
|
347
|
+
"tox",
|
|
348
|
+
(
|
|
349
|
+
"constrain_package_deps",
|
|
350
|
+
"constraints_file",
|
|
351
|
+
"envlist_lines",
|
|
352
|
+
"testenv_options",
|
|
353
|
+
"use_mxdev",
|
|
354
|
+
"config_lines",
|
|
355
|
+
"test_deps_additional",
|
|
356
|
+
"test_extras",
|
|
357
|
+
"test_environment_variables",
|
|
358
|
+
"extra_lines",
|
|
359
|
+
"use_pytest_plone",
|
|
360
|
+
"package_name",
|
|
361
|
+
),
|
|
362
|
+
)
|
|
363
|
+
use_mxdev = options.get("use_mxdev", False)
|
|
364
|
+
options.update(self._test_cfg())
|
|
365
|
+
options["package_name"] = options.get("package_name") or self.path.name
|
|
366
|
+
options["news_folder_exists"] = (self.path / "news").exists()
|
|
367
|
+
|
|
368
|
+
options["prime_robotframework"] = self._detect_robotframework()
|
|
369
|
+
|
|
370
|
+
if not options["constrain_package_deps"]:
|
|
371
|
+
options["constrain_package_deps"] = "false" if use_mxdev else "true"
|
|
372
|
+
if not options["constraints_file"]:
|
|
373
|
+
constraints_file = MXDEV_CONSTRAINTS if use_mxdev else PLONE_CONSTRAINTS
|
|
374
|
+
options["constraints_file"] = constraints_file
|
|
375
|
+
if options["use_pytest_plone"] is not False:
|
|
376
|
+
# Default is '', so turn it into True
|
|
377
|
+
options["use_pytest_plone"] = True
|
|
378
|
+
|
|
379
|
+
return self.copy_with_meta("tox.ini.j2", **options)
|
|
380
|
+
|
|
381
|
+
def _detect_robotframework(self):
|
|
382
|
+
"""Dynamically find out if robotframework is used in the package.
|
|
383
|
+
|
|
384
|
+
We look at the dependencies, as we expect the dependency checker
|
|
385
|
+
to make it easy for us.
|
|
386
|
+
"""
|
|
387
|
+
try_files = (
|
|
388
|
+
self.path / "setup.py",
|
|
389
|
+
self.path / "pyproject.toml",
|
|
390
|
+
)
|
|
391
|
+
for file_obj in try_files:
|
|
392
|
+
if file_obj.exists():
|
|
393
|
+
text = file_obj.read_text()
|
|
394
|
+
if "plone.app.robotframework" in text:
|
|
395
|
+
return True
|
|
396
|
+
return False
|
|
397
|
+
|
|
398
|
+
def news_entry(self):
|
|
399
|
+
news = self.path / "news"
|
|
400
|
+
|
|
401
|
+
if not news.exists():
|
|
402
|
+
return
|
|
403
|
+
|
|
404
|
+
if self.args.branch_name == "current":
|
|
405
|
+
print("Updating current branch, so I do not add a news entry.")
|
|
406
|
+
return
|
|
407
|
+
|
|
408
|
+
destination = self.path / "news" / "+meta.internal"
|
|
409
|
+
with open(destination, "w") as f_:
|
|
410
|
+
if (self.path / "CHANGES.md").exists():
|
|
411
|
+
f_.write("Update configuration files @plone")
|
|
412
|
+
else:
|
|
413
|
+
f_.write("Update configuration files.\n")
|
|
414
|
+
f_.write("[plone devs]\n")
|
|
415
|
+
|
|
416
|
+
return destination.relative_to(self.path)
|
|
417
|
+
|
|
418
|
+
def gha_workflows(self):
|
|
419
|
+
if not self.is_github:
|
|
420
|
+
return []
|
|
421
|
+
github_folder = self.path / ".github"
|
|
422
|
+
workflows_folder = github_folder / "workflows"
|
|
423
|
+
workflows_folder.mkdir(parents=True, exist_ok=True)
|
|
424
|
+
destination = workflows_folder / "meta.yml"
|
|
425
|
+
options = self._get_options_for(
|
|
426
|
+
"github",
|
|
427
|
+
(
|
|
428
|
+
"env",
|
|
429
|
+
"ref",
|
|
430
|
+
"jobs",
|
|
431
|
+
"os_dependencies",
|
|
432
|
+
"py_versions",
|
|
433
|
+
"extra_lines",
|
|
434
|
+
),
|
|
435
|
+
)
|
|
436
|
+
if not options.get("ref"):
|
|
437
|
+
options["ref"] = GHA_DEFAULT_REF
|
|
438
|
+
if not options.get("jobs"):
|
|
439
|
+
options["jobs"] = GHA_DEFAULT_JOBS
|
|
440
|
+
meta_file = self.copy_with_meta(
|
|
441
|
+
"meta.yml.j2", destination=destination, **options
|
|
442
|
+
)
|
|
443
|
+
dependabot = self.copy_with_meta(
|
|
444
|
+
"dependabot.yml", destination=github_folder / "dependabot.yml"
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
if (self.path / "dependabot.yml").exists():
|
|
448
|
+
self.print_warning(
|
|
449
|
+
"CI configuration",
|
|
450
|
+
"A `dependabot.yml` file at the top-level was found, please remove it",
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
return [meta_file, dependabot]
|
|
454
|
+
|
|
455
|
+
def gitlab_ci(self):
|
|
456
|
+
if not self.is_gitlab:
|
|
457
|
+
return []
|
|
458
|
+
options = self._get_options_for(
|
|
459
|
+
"gitlab",
|
|
460
|
+
(
|
|
461
|
+
"custom_image",
|
|
462
|
+
"os_dependencies",
|
|
463
|
+
"extra_lines",
|
|
464
|
+
"jobs",
|
|
465
|
+
),
|
|
466
|
+
)
|
|
467
|
+
if not options["custom_image"]:
|
|
468
|
+
options["custom_image"] = DOCKER_IMAGE
|
|
469
|
+
options["destination"] = self.path / ".gitlab-ci.yml"
|
|
470
|
+
if not options.get("jobs"):
|
|
471
|
+
options["jobs"] = GITLAB_DEFAULT_JOBS
|
|
472
|
+
return self.copy_with_meta("gitlab-ci.yml.j2", **options)
|
|
473
|
+
|
|
474
|
+
def flake8(self):
|
|
475
|
+
options = self._get_options_for("flake8", ("extra_lines",))
|
|
476
|
+
destination = self.path / ".flake8"
|
|
477
|
+
return self.copy_with_meta("flake8.j2", destination=destination, **options)
|
|
478
|
+
|
|
479
|
+
def copy_with_meta(
|
|
480
|
+
self, template_name, destination=None, meta_hint=META_HINT, **kw
|
|
481
|
+
):
|
|
482
|
+
"""Copy the source file to destination and a hint of origin.
|
|
483
|
+
|
|
484
|
+
If kwargs are given they are used as template arguments.
|
|
485
|
+
"""
|
|
486
|
+
template = self.jinja_env.get_template(template_name)
|
|
487
|
+
rendered = template.render(config_type=self.config_type, **kw)
|
|
488
|
+
meta_hint = meta_hint.format(config_type=self.config_type)
|
|
489
|
+
if rendered.startswith("#!"):
|
|
490
|
+
she_bang, _, body = rendered.partition("\n")
|
|
491
|
+
content = "\n".join([she_bang, meta_hint, body])
|
|
492
|
+
else:
|
|
493
|
+
content = "\n".join([meta_hint, rendered])
|
|
494
|
+
|
|
495
|
+
if destination is None:
|
|
496
|
+
if template_name.endswith(".j2"):
|
|
497
|
+
destination = self.path / template_name[:-3] # remove `.j2`
|
|
498
|
+
else:
|
|
499
|
+
destination = self.path / template_name
|
|
500
|
+
|
|
501
|
+
# Get rid of spaces on lines with only spaces, like happens in the generated
|
|
502
|
+
# tox.ini
|
|
503
|
+
content = re.sub(r"\n\ +\n", r"\n\n", content)
|
|
504
|
+
|
|
505
|
+
# Get rid of empty lines at the end.
|
|
506
|
+
content = content.strip() + "\n"
|
|
507
|
+
with open(destination, "w") as f_:
|
|
508
|
+
f_.write(content)
|
|
509
|
+
|
|
510
|
+
return destination.relative_to(self.path)
|
|
511
|
+
|
|
512
|
+
def remove_old_files(self):
|
|
513
|
+
filenames = ("bootstrap.py", ".travis.yml")
|
|
514
|
+
with change_dir(self.path):
|
|
515
|
+
for filename in filenames:
|
|
516
|
+
if pathlib.Path(filename).exists():
|
|
517
|
+
call("git", "rm", filename)
|
|
518
|
+
|
|
519
|
+
def remove_toml_empty_sections(self):
|
|
520
|
+
meta_cfg = {k: v for k, v in self.meta_cfg.items() if v}
|
|
521
|
+
with change_dir(self.path):
|
|
522
|
+
with open(".meta.toml", "w") as meta_f:
|
|
523
|
+
meta_f.write(META_HINT.format(config_type=self.config_type))
|
|
524
|
+
meta_f.write("\n")
|
|
525
|
+
tomlkit.dump(meta_cfg, meta_f)
|
|
526
|
+
|
|
527
|
+
def run_tox(self):
|
|
528
|
+
with change_dir(self.path) as cwd:
|
|
529
|
+
tox_path = shutil.which("tox") or (pathlib.Path(cwd) / "bin" / "tox")
|
|
530
|
+
call(tox_path, "-e", "format,lint")
|
|
531
|
+
|
|
532
|
+
def validate_files(self, files_changed):
|
|
533
|
+
"""Ensure that files are not broken"""
|
|
534
|
+
for file_obj in files_changed:
|
|
535
|
+
if file_obj.suffix == ".toml":
|
|
536
|
+
self._validate_toml(file_obj)
|
|
537
|
+
elif file_obj.suffix in (".yaml", ".yml"):
|
|
538
|
+
self._validate_yaml(file_obj)
|
|
539
|
+
elif file_obj.suffix == ".ini" or file_obj.stem == ".flake8":
|
|
540
|
+
self._validate_ini(file_obj)
|
|
541
|
+
elif file_obj.stem == ".editorconfig":
|
|
542
|
+
self._validate_editorconfig(file_obj)
|
|
543
|
+
|
|
544
|
+
def _validate_toml(self, file_obj):
|
|
545
|
+
"""Validate files that are in TOML format"""
|
|
546
|
+
with change_dir(self.path):
|
|
547
|
+
|
|
548
|
+
with open(file_obj, "rb") as meta_f:
|
|
549
|
+
data = tomlkit.load(meta_f)
|
|
550
|
+
|
|
551
|
+
if self.path.stem == "pyproject":
|
|
552
|
+
validator = validate_pyproject.api.Validator()
|
|
553
|
+
validator(data)
|
|
554
|
+
|
|
555
|
+
def _validate_yaml(self, file_obj):
|
|
556
|
+
"""Validate files that are in YAML format"""
|
|
557
|
+
with change_dir(self.path):
|
|
558
|
+
data = file_obj.read_text()
|
|
559
|
+
_ = yaml.safe_load(data)
|
|
560
|
+
|
|
561
|
+
def _validate_ini(self, file_obj):
|
|
562
|
+
"""Validate files that are in INI format"""
|
|
563
|
+
config = configparser.ConfigParser()
|
|
564
|
+
with change_dir(self.path):
|
|
565
|
+
_ = config.read(file_obj)
|
|
566
|
+
|
|
567
|
+
def _validate_editorconfig(self, file_obj):
|
|
568
|
+
"""Validate .editorconfig file"""
|
|
569
|
+
with change_dir(self.path):
|
|
570
|
+
editorconfig.get_properties(file_obj.resolve())
|
|
571
|
+
|
|
572
|
+
@property
|
|
573
|
+
def _commit_msg(self):
|
|
574
|
+
return self.args.commit_msg or "Configuring with plone.meta"
|
|
575
|
+
|
|
576
|
+
def commit_and_push(self, filenames):
|
|
577
|
+
if not self.args.commit:
|
|
578
|
+
return
|
|
579
|
+
|
|
580
|
+
with change_dir(self.path):
|
|
581
|
+
call("git", "add", *filenames)
|
|
582
|
+
call("git", "commit", "-m", self._commit_msg)
|
|
583
|
+
if self.args.push:
|
|
584
|
+
call("git", "push", "--set-upstream", "origin", self.branch_name)
|
|
585
|
+
|
|
586
|
+
@staticmethod
|
|
587
|
+
def final_help_tips(updating):
|
|
588
|
+
print()
|
|
589
|
+
print("If everything went fine up to here:")
|
|
590
|
+
if updating:
|
|
591
|
+
print("Updated the previously created PR.")
|
|
592
|
+
else:
|
|
593
|
+
print("Create a PR, using the URL shown above.")
|
|
594
|
+
|
|
595
|
+
def configure(self):
|
|
596
|
+
if self.args.track_package:
|
|
597
|
+
self._add_project_to_config_type_list()
|
|
598
|
+
|
|
599
|
+
files_changed = [
|
|
600
|
+
self.path / ".meta.toml",
|
|
601
|
+
]
|
|
602
|
+
methods = (
|
|
603
|
+
self.editorconfig,
|
|
604
|
+
self.gitignore,
|
|
605
|
+
self.pre_commit_config,
|
|
606
|
+
self.pyproject_toml,
|
|
607
|
+
self.tox,
|
|
608
|
+
self.news_entry,
|
|
609
|
+
self.flake8,
|
|
610
|
+
self.gha_workflows,
|
|
611
|
+
self.gitlab_ci,
|
|
612
|
+
)
|
|
613
|
+
for method in methods:
|
|
614
|
+
files = method()
|
|
615
|
+
if isinstance(files, list):
|
|
616
|
+
files_changed.extend(files)
|
|
617
|
+
else:
|
|
618
|
+
files_changed.append(files)
|
|
619
|
+
files_changed = [x for x in filter(None, files_changed)]
|
|
620
|
+
|
|
621
|
+
self.remove_old_files()
|
|
622
|
+
self.remove_toml_empty_sections()
|
|
623
|
+
if self.args.run_tox:
|
|
624
|
+
self.run_tox()
|
|
625
|
+
|
|
626
|
+
with change_dir(self.path):
|
|
627
|
+
updating = git_branch(self.branch_name)
|
|
628
|
+
|
|
629
|
+
self.validate_files(files_changed)
|
|
630
|
+
self.commit_and_push(files_changed)
|
|
631
|
+
self.warn_on_setup_cfg()
|
|
632
|
+
self.final_help_tips(updating)
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
def main():
|
|
636
|
+
args = handle_command_line_arguments()
|
|
637
|
+
|
|
638
|
+
package = PackageConfiguration(args)
|
|
639
|
+
package.configure()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{% if sections[""] %}
|
|
2
|
+
{% for category, val in definitions.items() if category in sections[""] %}
|
|
3
|
+
|
|
4
|
+
### {{ definitions[category]['name'] }}
|
|
5
|
+
|
|
6
|
+
{% for text, values in sections[""][category].items() %}
|
|
7
|
+
- {{ text }} {{ values|join(', ') }}
|
|
8
|
+
{% endfor %}
|
|
9
|
+
|
|
10
|
+
{% endfor %}
|
|
11
|
+
{% else %}
|
|
12
|
+
No significant changes.
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
{% endif %}
|