Skip to content

Commit 54edd6e

Browse files
authored
Remove black, use ruff for generated config_models (DataDog#23588)
* Drop black as a direct dependency, use ruff format for generated config models - Remove explicit `black==23.12.1` from `datadog_checks_dev` - Replace `apply_black` calls in the model consumer with `ruff format -`, using the repo's centralized `[tool.ruff]` configuration - Drop the now-unused `code_formatter` plumbing through `ModelConsumer` / `build_model_file` / `validate models` - Drop the [tool.black] block from `ddev/pyproject.toml` and the matching python-version-bump logic in `update_py_config.py` (with test fixture) - Update README badge from black to ruff The root `[tool.black]` section is kept (with an explanatory comment) because `datamodel-code-generator` reads it transitively through its own internal formatter, and removing it changes line-length to 88 which breaks our list[...] -> tuple[..., ...] line-by-line transform. * Add changelog entries * Make ruff a hard dep of datadog_checks_dev[cli], invoke via python -m ruff CI installs ddev with `pip install -e ./datadog_checks_dev[cli]` and never adds ruff to PATH. The previous helper called `shutil.which('ruff')` and silently returned the input unchanged when ruff was missing — leaving long lines and missing wraps in 401 generated config-model files, surfacing as "not in sync" in the validate workflow. - Declare `ruff>=0.11` in `datadog_checks_dev[cli]` so the package is always installed alongside the model generator. - Switch the helper to `sys.executable -m ruff` so the in-venv package is used regardless of PATH. - Raise loudly on missing ruff or non-zero ruff exit instead of silently degrading, so any future regression fails the workflow with a clear error. * Pin ruff to 0.11.10 to match ddev's hatch lint env * Remove [tool.black] from root pyproject.toml and update _fix_types Make `_fix_types` operate on the joined document (as UTF-8 bytes) instead of line by line, so the bracket-tracking pass works regardless of how datamodel-code-generator's internal formatter wrapped `list[...]`. Place the `, ...` sentinel right after the last non-whitespace byte before the closing `]`, so output stays on the previous content line even when the parser pre-wrapped the closing bracket onto its own line. With those changes the generator no longer relies on `[tool.black]` existing in the repo, so the section and its accompanying comment are removed from `pyproject.toml`. The black-related comment near the config_models lint exclusion and the black badge in `README.md` go too. Four config_models files (kafka_actions, win32_event_log, yarn x2) regenerate with different — but semantically identical — wrapping. They were the only ones whose pre-wrapped form was sensitive to the change in upstream line-length default; future regens are stable. * Add changelogs for regenerated config_models in kafka_actions, win32_event_log, yarn * Address PR review: docs, error-path hint, focused _fix_types tests - Replace remaining "code style - black" references in developer docs (`docs/developer/index.md` badge, `docs/developer/guidelines/style.md` style section, link reference) with ruff equivalents. - Update the stale `ddev test postgres -l` example output in `docs/developer/testing.md` to drop `black==22.12.0` and reflect the current lint env contents (`ruff==0.11.10`, `pydantic==2.11.5`). - Move the "ruff is not installed" install hint in `format_with_ruff` from the `FileNotFoundError` branch to the `CalledProcessError` branch and gate it on `"No module named 'ruff'"` in stderr — the previous layout was effectively dead code because `sys.executable` always resolves, so missing-ruff surfaces as a non-zero exit, not a missing binary. - Add `tests/tooling/configuration/consumers/model/test_fix_types.py` with focused coverage for the `_fix_types` post-processing pass: the multiline-wrapped `list[Literal[...]]` regression case the PR was written to fix, dict and nested-list translations, unicode in descriptions, and verbatim pass-through when no `list[`/`dict[` is present. * Address PR review (round 2): code_formatter robustness + direct tests code_formatter.py: - Guard `_resolve_ruff_config` with `if root_str:` so an unset `get_root()` (returns '') doesn't fall into the `Path('').is_dir()` branch (which is True — it resolves to CWD), and unit tests actually walk back to the repo pyproject.toml as the docstring claims. - Replace the loose `'[tool.ruff' in text` substring with a line-anchored scan that only matches actual TOML table headers (`[tool.ruff]` or `[tool.ruff.…]`), so a comment or string value can't false-positive. - Surface argv (via `shlex.join`), stderr, and stdout in the error message for non-missing-package failures, so a future ruff config change emitting actionable output is debuggable from the message alone. Tests: - New `test_code_formatter.py` (17 tests): direct coverage for `format_with_ruff` (line wrapping, quote-style preservation, short passthrough, missing-ruff hint, full-context error on other failures) and `_resolve_ruff_config` (root path success, fallback walk on empty root, fallback walk when root has no `[tool.ruff]`, returns None when nothing is found, parametrized header recognition for `_has_ruff_section`). - `test_update_py_config.py`: add explicit content assertions on the rewritten `ddev/pyproject.toml` — no `[tool.black]` block survives, `[tool.ruff].target-version` is updated to the new pinned version, the old token is gone. Captures the actual contract instead of relying on the success-counter being 9. * Relax ddev's datadog-checks-dev pin to span the dcd 39 release gap This PR bumps `datadog-checks-dev` to 39 (`black` is dropped from `[cli]` extras, so per semver it's a major). The current `~=38.0` constraint in ddev's `pyproject.toml` would block any GitHub Action that installs both packages from the local repo — `pip install -e ./ddev` would fail to resolve the local dcd 39 against ddev's pin. Relax the pin to `>=38.0,<40` for the duration of the gap between this PR landing and the next ddev release. The release PR for ddev MUST tighten this back to `~=39.0`. * Apply ruff format to new helper and tests * Restore relaxed datadog-checks-dev pin for transition period
1 parent 2f66fc4 commit 54edd6e

27 files changed

Lines changed: 522 additions & 134 deletions

File tree

README.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
| --- | --- |
55
| CI/CD | [![CI - Test][1]][2] |
66
| Docs | [![Docs - Release][19]][20] |
7-
| Meta | [![Hatch project][26]][27] [![Linting - Ruff][24]][25] [![Code style - black][21]][22] [![Typing - Mypy][28]][29] [![License - BSD-3-Clause][30]][31] |
7+
| Meta | [![Hatch project][26]][27] [![Linting - Ruff][24]][25] [![Typing - Mypy][28]][29] [![License - BSD-3-Clause][30]][31] |
88

99
This repository contains open source integrations that Datadog officially develops and supports.
1010
To add a new integration, please see the [Integrations Extras][5] repository and the
@@ -43,10 +43,8 @@ For more information on integrations, please reference our [documentation][11] a
4343
[16]: https://github.com/DataDog/integrations-core/blob/ea2dfbf1e8859333af4c8db50553eb72a3b466f9/requirements-agent-release.txt
4444
[19]: https://github.com/DataDog/integrations-core/workflows/docs/badge.svg
4545
[20]: https://github.com/DataDog/integrations-core/actions?workflow=docs
46-
[21]: https://img.shields.io/badge/code%20style-black-000000.svg
47-
[22]: https://github.com/ambv/black
48-
[24]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v0.json
49-
[25]: https://github.com/charliermarsh/ruff
46+
[24]: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json
47+
[25]: https://github.com/astral-sh/ruff
5048
[26]: https://img.shields.io/badge/%F0%9F%A5%9A-Hatch-4051b5.svg
5149
[27]: https://github.com/pypa/hatch
5250
[28]: https://img.shields.io/badge/typing-Mypy-blue.svg
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Stop declaring `black` as a direct dependency. The `apply_black` calls used to format auto-generated config-model files now go through `ruff format`, using the repo's centralized `[tool.ruff]` configuration.

datadog_checks_dev/datadog_checks/dev/tooling/commands/validate/models.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,6 @@ def models(ctx, check, sync, verbose):
9595
license_header_lines = get_license_header().splitlines(True) + ['\n', '\n']
9696
documentation_header_lines = get_config_models_documentation().splitlines(True) + ['\n']
9797

98-
code_formatter = ModelConsumer.create_code_formatter()
99-
10098
if is_core_check:
10199
checks = checks.difference(INTEGRATIONS_WITHOUT_MODELS)
102100

@@ -135,7 +133,7 @@ def models(ctx, check, sync, verbose):
135133
if not sync and not dir_exists(models_location) and not is_core_check:
136134
continue
137135

138-
model_consumer = ModelConsumer(spec.data, code_formatter)
136+
model_consumer = ModelConsumer(spec.data)
139137

140138
# So formatters see config files
141139
with chdir(root):
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# (C) Datadog, Inc. 2026-present
2+
# All rights reserved
3+
# Licensed under a 3-clause BSD style license (see LICENSE)
4+
import shlex
5+
import subprocess
6+
import sys
7+
from pathlib import Path
8+
9+
from datadog_checks.dev.tooling.constants import get_root
10+
11+
12+
def format_with_ruff(source: str) -> str:
13+
"""Format Python source via ``ruff format -`` (stdin/stdout).
14+
15+
Replaces the line-wrapping role previously played by black on auto-generated
16+
config_models files. Uses the repo's centralized ruff configuration so the
17+
output matches the rest of the codebase. Invokes ruff through the active
18+
interpreter (``python -m ruff``) so the package installed alongside
19+
``datadog_checks_dev[cli]`` is always picked up, regardless of PATH.
20+
"""
21+
args = [sys.executable, '-m', 'ruff', 'format', '--quiet', '--stdin-filename=model.py']
22+
config_path = _resolve_ruff_config()
23+
if config_path is not None:
24+
args.extend(['--config', str(config_path)])
25+
else:
26+
args.extend(['--isolated', '--config', "format.quote-style='preserve'", '--line-length=120'])
27+
args.append('-')
28+
29+
try:
30+
result = subprocess.run(
31+
args,
32+
input=source,
33+
capture_output=True,
34+
text=True,
35+
check=True,
36+
)
37+
except subprocess.CalledProcessError as e:
38+
# `python -m ruff` exits non-zero when the ruff package is missing,
39+
# surfacing as ModuleNotFoundError on stderr. Promote that to a
40+
# clearer install hint; otherwise propagate the underlying error
41+
# with enough context to reproduce the failure manually.
42+
stderr = e.stderr or ''
43+
if "No module named 'ruff'" in stderr:
44+
raise RuntimeError(
45+
"Cannot format auto-generated config models: the `ruff` package is not installed in the active "
46+
"interpreter. Reinstall `datadog_checks_dev[cli]` (or run `pip install ruff`) and retry."
47+
) from e
48+
details = [f'{shlex.join(args)} failed', f'stderr: {stderr.strip()}']
49+
if e.stdout:
50+
details.append(f'stdout: {e.stdout.strip()}')
51+
raise RuntimeError(
52+
'`ruff format` failed while formatting auto-generated config models. ' + '; '.join(details)
53+
) from e
54+
return result.stdout
55+
56+
57+
def _resolve_ruff_config() -> Path | None:
58+
"""Locate the repo pyproject.toml that holds the central ruff configuration.
59+
60+
Prefer the path reported by ``get_root`` (set by ddev commands). Fall back
61+
to walking up from this module so unit tests, which never call ``set_root``,
62+
still pick up the same configuration as model regeneration.
63+
"""
64+
root_str = get_root()
65+
if root_str:
66+
root = Path(root_str)
67+
if root.is_dir():
68+
candidate = root / 'pyproject.toml'
69+
if _has_ruff_section(candidate):
70+
return candidate
71+
72+
for parent in Path(__file__).resolve().parents:
73+
candidate = parent / 'pyproject.toml'
74+
if _has_ruff_section(candidate):
75+
return candidate
76+
return None
77+
78+
79+
def _has_ruff_section(pyproject: Path) -> bool:
80+
if not pyproject.is_file():
81+
return False
82+
try:
83+
text = pyproject.read_text()
84+
except OSError:
85+
return False
86+
return any(
87+
stripped == '[tool.ruff]' or stripped.startswith('[tool.ruff.')
88+
for stripped in (line.strip() for line in text.splitlines())
89+
)

datadog_checks_dev/datadog_checks/dev/tooling/configuration/consumers/model/model_consumer.py

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,19 @@
22
# All rights reserved
33
# Licensed under a 3-clause BSD style license (see LICENSE)
44
import warnings
5-
from pathlib import Path
65
from typing import Dict, List, Tuple
76

87
import yaml
98
from datamodel_code_generator import DataModelType
10-
from datamodel_code_generator.format import CodeFormatter, PythonVersion
9+
from datamodel_code_generator.format import PythonVersion
1110
from datamodel_code_generator.model import get_data_model_types
1211
from datamodel_code_generator.parser import LiteralType
1312
from datamodel_code_generator.parser.openapi import OpenAPIParser
1413

14+
from datadog_checks.dev.tooling.configuration.consumers.model.code_formatter import format_with_ruff
1515
from datadog_checks.dev.tooling.configuration.consumers.model.model_file import build_model_file
1616
from datadog_checks.dev.tooling.configuration.consumers.model.model_info import ModelInfo
1717
from datadog_checks.dev.tooling.configuration.consumers.openapi_document import build_openapi_document
18-
from datadog_checks.dev.tooling.constants import get_root
1918

2019
PYTHON_VERSION = PythonVersion.PY_39
2120

@@ -32,9 +31,8 @@
3231

3332

3433
class ModelConsumer:
35-
def __init__(self, spec: dict, code_formatter: CodeFormatter = None):
34+
def __init__(self, spec: dict):
3635
self.spec = spec
37-
self.code_formatter = code_formatter or self.create_code_formatter()
3836

3937
def render(self) -> Dict[str, Dict[str, str]]:
4038
"""
@@ -137,7 +135,6 @@ def _process_section(self, section) -> (List[Tuple[str, str]], dict, ModelInfo):
137135
model_id,
138136
section_name,
139137
model_info,
140-
self.code_formatter,
141138
)
142139
# instance.py or shared.py
143140
model_files[model_file_name] = (model_file_contents, errors)
@@ -207,11 +204,6 @@ def _merge_instances(self, section: dict, errors: List[str]) -> dict:
207204

208205
return new_section
209206

210-
@staticmethod
211-
def create_code_formatter():
212-
path = Path(get_root())
213-
return CodeFormatter(PYTHON_VERSION, settings_path=path if path.is_dir() else None)
214-
215207
def _build_deprecation_file(self, deprecation_data):
216208
file_needs_formatting = False
217209
deprecations_file_lines = []
@@ -226,7 +218,7 @@ def _build_deprecation_file(self, deprecation_data):
226218
deprecations_file_lines.append('')
227219
deprecations_file_contents = '\n'.join(deprecations_file_lines)
228220
if file_needs_formatting:
229-
deprecations_file_contents = self.code_formatter.apply_black(deprecations_file_contents)
221+
deprecations_file_contents = format_with_ruff(deprecations_file_contents)
230222
return deprecations_file_contents
231223

232224
@staticmethod
@@ -255,5 +247,5 @@ def _build_defaults_file(self, model_info: ModelInfo):
255247
model_info.defaults_file_lines.append('')
256248
defaults_file_contents = '\n'.join(model_info.defaults_file_lines)
257249
if model_info.defaults_file_needs_value_normalization:
258-
defaults_file_contents = self.code_formatter.apply_black(defaults_file_contents)
250+
defaults_file_contents = format_with_ruff(defaults_file_contents)
259251
return defaults_file_contents

datadog_checks_dev/datadog_checks/dev/tooling/configuration/consumers/model/model_file.py

Lines changed: 40 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
# (C) Datadog, Inc. 2021-present
22
# All rights reserved
33
# Licensed under a 3-clause BSD style license (see LICENSE)
4-
from datamodel_code_generator.format import CodeFormatter
5-
4+
from datadog_checks.dev.tooling.configuration.consumers.model.code_formatter import format_with_ruff
65
from datadog_checks.dev.tooling.configuration.consumers.model.model_info import ModelInfo
76

87

@@ -11,14 +10,12 @@ def build_model_file(
1110
model_id: str,
1211
section_name: str,
1312
model_info: ModelInfo,
14-
code_formatter: CodeFormatter,
1513
):
1614
"""
1715
:param parsed_document: OpenApi parsed document
1816
:param model_id: instance or shared
1917
:param section_name: init or instances
2018
:param model_info: Information to build the model file
21-
:param code_formatter:
2219
"""
2320
# Whether or not there are options with default values
2421
options_with_defaults = len(model_info.defaults_file_lines) > 0
@@ -54,7 +51,7 @@ def build_model_file(
5451
model_file_lines.append('')
5552
model_file_contents = '\n'.join(model_file_lines)
5653
if any(len(line) > 120 for line in model_file_lines):
57-
model_file_contents = code_formatter.apply_black(model_file_contents)
54+
model_file_contents = format_with_ruff(model_file_contents)
5855
return model_file_contents
5956

6057

@@ -109,27 +106,44 @@ def _add_imports(model_file_lines, need_defaults, need_deprecations):
109106

110107

111108
def _fix_types(model_file_lines):
112-
for i, line in enumerate(model_file_lines):
113-
line = model_file_lines[i] = line.replace('dict[', 'MappingProxyType[')
114-
if 'list[' not in line:
115-
continue
116-
117-
buffer = bytearray()
118-
containers = []
119-
120-
for char in line:
121-
if char == '[':
122-
if buffer[-4:] == b'list':
123-
containers.append(True)
124-
buffer[-4:] = b'tuple'
125-
else:
126-
containers.append(False)
127-
elif char == ']' and containers.pop():
128-
buffer.extend(b', ...')
129-
130-
buffer.append(ord(char))
131-
132-
model_file_lines[i] = buffer.decode('utf-8')
109+
# Operate on the joined document (as UTF-8 bytes) so the bracket-tracking
110+
# pass below works even when the upstream parser pre-wraps `list[...]`
111+
# across multiple lines. Iterating bytes keeps the algorithm safe for
112+
# non-ASCII content (descriptions, examples) since `[`, `]`, and `list`
113+
# are all single-byte ASCII while UTF-8 continuation bytes never collide
114+
# with them.
115+
content = '\n'.join(model_file_lines).replace('dict[', 'MappingProxyType[')
116+
if 'list[' not in content:
117+
model_file_lines[:] = content.split('\n')
118+
return
119+
120+
encoded = content.encode('utf-8')
121+
buffer = bytearray()
122+
containers = []
123+
open_bracket = ord(b'[')
124+
close_bracket = ord(b']')
125+
whitespace = (ord(b' '), ord(b'\t'), ord(b'\n'))
126+
127+
for byte in encoded:
128+
if byte == open_bracket:
129+
if buffer[-4:] == b'list':
130+
containers.append(True)
131+
buffer[-4:] = b'tuple'
132+
else:
133+
containers.append(False)
134+
elif byte == close_bracket and containers and containers.pop():
135+
# Insert `, ...` after the last non-whitespace byte already in the
136+
# buffer so the sentinel sits on the same line as the previous
137+
# content (`tuple[X], ...` style) even when the parser wrapped the
138+
# closing `]` onto its own line.
139+
insert_at = len(buffer)
140+
while insert_at > 0 and buffer[insert_at - 1] in whitespace:
141+
insert_at -= 1
142+
buffer[insert_at:insert_at] = b', ...'
143+
144+
buffer.append(byte)
145+
146+
model_file_lines[:] = buffer.decode('utf-8').split('\n')
133147

134148

135149
def _add_secure_fields_constant(model_file_lines, require_trusted_providers):

datadog_checks_dev/pyproject.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ cli = [
5252
"aiomultiprocess",
5353
"atomicwrites",
5454
"beautifulsoup4==4.12.3",
55-
"black==23.12.1", # TODO Remove once https://github.com/koxudaxi/datamodel-code-generator/issues/1821 is fixed
5655
"build>=0.7.0",
5756
"click~=8.1.6",
5857
"codespell",
@@ -69,6 +68,13 @@ cli = [
6968
"platformdirs>=2.0.0a3",
7069
"pydantic>=2.0.2",
7170
"pysmi==1.6.2",
71+
# ruff is invoked as a subprocess by the model generator to format
72+
# auto-generated config_models files (see datadog_checks/dev/tooling/
73+
# configuration/consumers/model/code_formatter.py). Keep this pin in sync
74+
# with the version used by ddev's hatch lint env (currently `ruff==0.11.10`
75+
# in ddev/src/ddev/plugin/external/hatch/environment_collector.py) until
76+
# the commands here are migrated into ddev itself.
77+
"ruff==0.11.10",
7278
"securesystemslib[crypto]==0.28.0",
7379
"semver>=2.13.0",
7480
"tabulate>=0.8.9",

0 commit comments

Comments
 (0)