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

Improve the upgrade-python script #16000

Merged
merged 3 commits into from
Oct 18, 2023
Merged
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
4 changes: 4 additions & 0 deletions ddev/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

***Added***:

* Improve the upgrade-python script ([#16000](https://github.com/DataDog/integrations-core/pull/16000))

## 5.2.1 / 2023-10-12

***Fixed***:
Expand Down
217 changes: 178 additions & 39 deletions ddev/src/ddev/cli/meta/scripts/upgrade_python.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

if TYPE_CHECKING:
from ddev.cli.application import Application
from ddev.src.ddev.validation.tracker import ValidationTracker


@click.command('upgrade-python', short_help='Upgrade the Python version throughout the repository')
Expand All @@ -20,57 +21,195 @@ def upgrade_python(app: Application, version: str):
\b
`$ ddev meta scripts upgrade-python 3.11`
"""
import tomlkit

from ddev.repo.constants import PYTHON_VERSION as old_version

tracker = app.create_validation_tracker('Python upgrades')

for target in app.repo.integrations.iter_testable(['all']):
config_file = target.path / 'hatch.toml'
test_config = tomlkit.parse(config_file.read_text())
changed = False

for env in test_config.get('envs', {}).values():
default_python = env.get('python', '')
if default_python == old_version:
env['python'] = version
tracker.success()
changed = True
update_hatch_file(app, target.path, version, old_version, tracker)
update_pyproject_file(target, version, old_version, tracker)
iliakur marked this conversation as resolved.
Show resolved Hide resolved
update_setup_file(target, version, old_version, tracker)

for variables in env.get('matrix', []):
pythons = variables.get('python', [])
for i, python in enumerate(pythons):
if python == old_version:
pythons[i] = version
tracker.success()
changed = True

for overrides in env.get('overrides', {}).get('matrix', {}).get('python', {}).values():
for override in overrides:
pythons = override.get('if', [])
for i, python in enumerate(pythons):
if python == old_version:
pythons[i] = version
tracker.success()
changed = True

if changed:
config_file.write_text(tomlkit.dumps(test_config))
update_ci_files(app, version, old_version, tracker)

if app.repo.name == 'core':
constant_file = app.repo.path / 'ddev' / 'src' / 'ddev' / 'repo' / 'constants.py'
update_ddev_pyproject_file(app, version, old_version, tracker)
update_constants_file(app, version, old_version, tracker)
update_ddev_template_files(app, version, old_version, tracker)
app.display_warning("Documentation files have not been updated. Please modify them manually.")

tracker.display()

if tracker.errors: # no cov
app.abort()


def update_ci_files(app: Application, new_version: str, old_version: str, tracker: ValidationTracker):
for file in (app.repo.path / ".github" / "workflows").glob("*.yml"):
old_content = new_content = file.read_text()

for pattern in ("python-version: '{}'", 'PYTHON_VERSION: "{}"', "'{}'"):
if pattern.format(old_version) in new_content:
new_content = new_content.replace(pattern.format(old_version), pattern.format(new_version))

if old_content != new_content:
file.write_text(new_content)
tracker.success()


def update_ddev_template_files(app: Application, new_version: str, old_version: str, tracker: ValidationTracker):
for check_type in ("check", "jmx", "logs"):
folder_path = (
app.repo.path
/ 'datadog_checks_dev'
/ 'datadog_checks'
/ 'dev'
/ 'tooling'
/ 'templates'
/ 'integration'
/ check_type
/ '{check_name}'
)
pyproject_file = folder_path / 'pyproject.toml'

lines = constant_file.read_text().splitlines(keepends=True)
for i, line in enumerate(lines):
if line.startswith('PYTHON_VERSION = '):
lines[i] = line.replace(old_version, version)
if pyproject_file.is_file():
old_content = new_content = pyproject_file.read_text()

for pattern in ('requires-python = ">={}"', "Programming Language :: Python :: {}"):
new_content = new_content.replace(pattern.format(old_version), pattern.format(new_version))

if old_content != new_content:
pyproject_file.write_text(new_content)
tracker.success()

if (folder_path / 'hatch.toml').is_file():
update_hatch_file(app, folder_path, new_version, old_version, tracker)


def update_ddev_pyproject_file(app: Application, new_version: str, old_version: str, tracker: ValidationTracker):
import tomlkit

config_file = app.repo.path / 'ddev' / 'pyproject.toml'
config = tomlkit.parse(config_file.read_text())
changed = False
new_version = f"py{new_version.replace('.', '')}"
old_version = f"py{old_version.replace('.', '')}"

if black_config := config.get('tool', {}).get('black', {}):
target_version = black_config.get('target-version', [])

for index, version in enumerate(target_version):
if version == old_version:
target_version[index] = new_version
tracker.success()
changed = True
break

constant_file.write_text(''.join(lines))
if ruff_config := config.get('tool', {}).get('ruff', {}):
if ruff_config.get('target-version') == old_version:
ruff_config['target-version'] = new_version
tracker.success()
changed = True

if changed:
config_file.write_text(tomlkit.dumps(config))


def update_setup_file(target, new_version: str, old_version: str, tracker: ValidationTracker):
setup_file = target.path / 'setup.py'

if setup_file.is_file():
content = setup_file.read_text()

if f"Programming Language :: Python :: {old_version}" in content:
content = content.replace(
f"Programming Language :: Python :: {old_version}", f"Programming Language :: Python :: {new_version}"
)

setup_file.write_text(content)
tracker.success()


def update_constants_file(app: Application, new_version: str, old_version: str, tracker: ValidationTracker):
constant_file = app.repo.path / 'ddev' / 'src' / 'ddev' / 'repo' / 'constants.py'

lines = constant_file.read_text().splitlines(keepends=True)
for i, line in enumerate(lines):
if line.startswith('PYTHON_VERSION = '):
lines[i] = line.replace(old_version, new_version)
break

constant_file.write_text(''.join(lines))
tracker.success()


def update_pyproject_file(target, new_version: str, old_version: str, tracker: ValidationTracker):
import tomlkit

config_file = target.path / 'pyproject.toml'
config = tomlkit.parse(config_file.read_text())
changed = False

classifiers = config.get('project', {}).get('classifiers', [])
for index, classifier in enumerate(classifiers):
if classifier == f"Programming Language :: Python :: {old_version}":
classifiers[index] = f"Programming Language :: Python :: {new_version}"
changed = True
tracker.success()
break

if changed:
config_file.write_text(tomlkit.dumps(config))


def update_hatch_file(app: Application, target_path, new_version: str, old_version: str, tracker: ValidationTracker):
import tomlkit

config_file = target_path / 'hatch.toml'
test_config = tomlkit.parse(config_file.read_text())
changed = False

for env in test_config.get('envs', {}).values():
if update_hatch_env(app, env, new_version, old_version, config_file, tracker):
changed = True

if changed:
config_file.write_text(tomlkit.dumps(test_config))


def update_hatch_env(
app: Application, env, new_version: str, old_version: str, config_file, tracker: ValidationTracker
) -> bool:
changed = False

default_python = env.get('python', '')
if default_python == old_version:
env['python'] = new_version
tracker.success()
changed = True

tracker.display()
for variables in env.get('matrix', []):
pythons = variables.get('python', [])
for i, python in enumerate(pythons):
if python == old_version:
pythons[i] = new_version
tracker.success()
changed = True

if tracker.errors: # no cov
app.abort()
for overrides in env.get('overrides', {}).get('matrix', {}).get('python', {}).values():
for override in overrides:
pythons = override.get('if', [])
for i, python in enumerate(pythons):
if python == old_version:
pythons[i] = new_version
tracker.success()
changed = True

if isinstance(env.get('overrides', {}), dict):
for name in list(env.get('overrides', {}).get('name', {}).keys()):
if f"py{old_version}" in name:
# TODO I don't find a way to keep the exact same format when I modify this.
iliakur marked this conversation as resolved.
Show resolved Hide resolved
app.display_warning(f'An override has been found in {config_file}. Please manually update it.')

return changed
66 changes: 66 additions & 0 deletions ddev/tests/cli/meta/scripts/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,72 @@ def fake_repo(tmp_path_factory, config_file, ddev):
[[envs.default.matrix]]
python = ["2.7", "3.9"]
""",
)

write_file(
repo_path / 'dummy',
'pyproject.toml',
"""[project]
name = "dummy"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"Intended Audience :: System Administrators",
"License :: OSI Approved :: BSD License",
"Natural Language :: English",
"Operating System :: OS Independent",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3.9",
]
""",
)

write_file(
repo_path / '.github' / 'workflows',
'build-ddev.yml',
"""name: build ddev
env:
APP_NAME: ddev
PYTHON_VERSION: "3.9"
PYOXIDIZER_VERSION: "0.24.0"
""",
)

write_file(
repo_path / 'ddev',
'pyproject.toml',
"""[tool.black]
target-version = ["py39"]
[tool.ruff]
target-version = "py39"
""",
)

write_file(
repo_path
/ 'datadog_checks_dev'
/ 'datadog_checks'
/ 'dev'
/ 'tooling'
/ 'templates'
/ 'integration'
/ 'check'
/ '{check_name}',
'pyproject.toml',
"""[project]
name = "dummy"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"Intended Audience :: System Administrators",
"License :: OSI Approved :: BSD License",
"Natural Language :: English",
"Operating System :: OS Independent",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3.9",
]
""",
)

Expand Down
28 changes: 27 additions & 1 deletion ddev/tests/cli/meta/scripts/test_upgrade_python.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,39 @@ def test_upgrade_python(fake_repo, ddev):
result = ddev('meta', 'scripts', 'upgrade-python', new_version)

assert result.exit_code == 0, result.output
assert result.output == 'Python upgrades\n\nPassed: 2\n'
assert result.output.endswith('Python upgrades\n\nPassed: 7\n')

contents = constant_file.read_text()
assert f'PYTHON_VERSION = {old_version!r}' not in contents
assert f'PYTHON_VERSION = {new_version!r}' in contents

ci_file = fake_repo.path / '.github' / 'workflows' / 'build-ddev.yml'
contents = ci_file.read_text()
assert f'PYTHON_VERSION: "{old_version}"' not in contents
assert f'PYTHON_VERSION: "{new_version}"' in contents

hatch_file = fake_repo.path / 'dummy' / 'hatch.toml'
contents = hatch_file.read_text()
assert f'python = ["2.7", "{old_version}"]' not in contents
assert f'python = ["2.7", "{new_version}"]' in contents

pyproject_file = fake_repo.path / 'dummy' / 'pyproject.toml'
contents = pyproject_file.read_text()
assert f'Programming Language :: Python :: {old_version}' not in contents
assert f'Programming Language :: Python :: {new_version}' in contents

template_file = (
fake_repo.path
/ 'datadog_checks_dev'
/ 'datadog_checks'
/ 'dev'
/ 'tooling'
/ 'templates'
/ 'integration'
/ 'check'
/ '{check_name}'
/ 'pyproject.toml'
)
contents = template_file.read_text()
assert f'Programming Language :: Python :: {old_version}' not in contents
assert f'Programming Language :: Python :: {new_version}' in contents