diff --git a/bin/blacken-docs.py b/bin/ruffen-docs.py similarity index 52% rename from bin/blacken-docs.py rename to bin/ruffen-docs.py index 45d0ad1225..37b3d94f0f 100644 --- a/bin/blacken-docs.py +++ b/bin/ruffen-docs.py @@ -1,16 +1,14 @@ -# fork of https://github.com/asottile/blacken-docs implementing https://github.com/asottile/blacken-docs/issues/170 +# fork of https://github.com/asottile/blacken-docs adapted for ruff from __future__ import annotations import re +import sys import argparse import textwrap import contextlib +import subprocess from typing import Match, Optional, Sequence, Generator, NamedTuple, cast -import black -from black.mode import TargetVersion -from black.const import DEFAULT_LINE_LENGTH - MD_RE = re.compile( r"(?P^(?P *)```\s*python\n)" r"(?P.*?)" r"(?P^(?P=indent)```\s*$)", re.DOTALL | re.MULTILINE, @@ -19,55 +17,12 @@ r"(?P^(?P *)```\s*pycon\n)" r"(?P.*?)" r"(?P^(?P=indent)```.*$)", re.DOTALL | re.MULTILINE, ) -RST_PY_LANGS = frozenset(("python", "py", "sage", "python3", "py3", "numpy")) -BLOCK_TYPES = "(code|code-block|sourcecode|ipython)" -DOCTEST_TYPES = "(testsetup|testcleanup|testcode)" -RST_RE = re.compile( - rf"(?P" - rf"^(?P *)\.\. (" - rf"jupyter-execute::|" - rf"{BLOCK_TYPES}:: (?P\w+)|" - rf"{DOCTEST_TYPES}::.*" - rf")\n" - rf"((?P=indent) +:.*\n)*" - rf"\n*" - rf")" - rf"(?P(^((?P=indent) +.*)?\n)+)", - re.MULTILINE, -) -RST_PYCON_RE = re.compile( - r"(?P" - r"(?P *)\.\. ((code|code-block):: pycon|doctest::.*)\n" - r"((?P=indent) +:.*\n)*" - r"\n*" - r")" - r"(?P(^((?P=indent) +.*)?(\n|$))+)", - re.MULTILINE, -) PYCON_PREFIX = ">>> " PYCON_CONTINUATION_PREFIX = "..." PYCON_CONTINUATION_RE = re.compile( rf"^{re.escape(PYCON_CONTINUATION_PREFIX)}( |$)", ) -LATEX_RE = re.compile( - r"(?P^(?P *)\\begin{minted}{python}\n)" - r"(?P.*?)" - r"(?P^(?P=indent)\\end{minted}\s*$)", - re.DOTALL | re.MULTILINE, -) -LATEX_PYCON_RE = re.compile( - r"(?P^(?P *)\\begin{minted}{pycon}\n)" r"(?P.*?)" r"(?P^(?P=indent)\\end{minted}\s*$)", - re.DOTALL | re.MULTILINE, -) -PYTHONTEX_LANG = r"(?Ppyblock|pycode|pyconsole|pyverbatim)" -PYTHONTEX_RE = re.compile( - rf"(?P^(?P *)\\begin{{{PYTHONTEX_LANG}}}\n)" - rf"(?P.*?)" - rf"(?P^(?P=indent)\\end{{(?P=lang)}}\s*$)", - re.DOTALL | re.MULTILINE, -) -INDENT_RE = re.compile("^ +(?=[^ ])", re.MULTILINE) -TRAILING_NL_RE = re.compile(r"\n+\Z", re.MULTILINE) +DEFAULT_LINE_LENGTH = 100 class CodeBlockError(NamedTuple): @@ -77,7 +32,6 @@ class CodeBlockError(NamedTuple): def format_str( src: str, - black_mode: black.FileMode, ) -> tuple[str, Sequence[CodeBlockError]]: errors: list[CodeBlockError] = [] @@ -91,24 +45,10 @@ def _collect_error(match: Match[str]) -> Generator[None, None, None]: def _md_match(match: Match[str]) -> str: code = textwrap.dedent(match["code"]) with _collect_error(match): - code = black.format_str(code, mode=black_mode) + code = format_code_block(code) code = textwrap.indent(code, match["indent"]) return f'{match["before"]}{code}{match["after"]}' - def _rst_match(match: Match[str]) -> str: - lang = match["lang"] - if lang is not None and lang not in RST_PY_LANGS: - return match[0] - min_indent = min(INDENT_RE.findall(match["code"])) - trailing_ws_match = TRAILING_NL_RE.search(match["code"]) - assert trailing_ws_match - trailing_ws = trailing_ws_match.group() - code = textwrap.dedent(match["code"]) - with _collect_error(match): - code = black.format_str(code, mode=black_mode) - code = textwrap.indent(code, min_indent) - return f'{match["before"]}{code.rstrip()}{trailing_ws}' - def _pycon_match(match: Match[str]) -> str: code = "" fragment = cast(Optional[str], None) @@ -119,7 +59,7 @@ def finish_fragment() -> None: if fragment is not None: with _collect_error(match): - fragment = black.format_str(fragment, mode=black_mode) + fragment = format_code_block(fragment) fragment_lines = fragment.splitlines() code += f"{PYCON_PREFIX}{fragment_lines[0]}\n" for line in fragment_lines[1:]: @@ -159,42 +99,33 @@ def _md_pycon_match(match: Match[str]) -> str: code = textwrap.indent(code, match["indent"]) return f'{match["before"]}{code}{match["after"]}' - def _rst_pycon_match(match: Match[str]) -> str: - code = _pycon_match(match) - min_indent = min(INDENT_RE.findall(match["code"])) - code = textwrap.indent(code, min_indent) - return f'{match["before"]}{code}' - - def _latex_match(match: Match[str]) -> str: - code = textwrap.dedent(match["code"]) - with _collect_error(match): - code = black.format_str(code, mode=black_mode) - code = textwrap.indent(code, match["indent"]) - return f'{match["before"]}{code}{match["after"]}' - - def _latex_pycon_match(match: Match[str]) -> str: - code = _pycon_match(match) - code = textwrap.indent(code, match["indent"]) - return f'{match["before"]}{code}{match["after"]}' - src = MD_RE.sub(_md_match, src) src = MD_PYCON_RE.sub(_md_pycon_match, src) - src = RST_RE.sub(_rst_match, src) - src = RST_PYCON_RE.sub(_rst_pycon_match, src) - src = LATEX_RE.sub(_latex_match, src) - src = LATEX_PYCON_RE.sub(_latex_pycon_match, src) - src = PYTHONTEX_RE.sub(_latex_match, src) return src, errors +def format_code_block(code: str) -> str: + return subprocess.check_output( + [ + sys.executable, + "-m", + "ruff", + "format", + "--stdin-filename=script.py", + f"--line-length={DEFAULT_LINE_LENGTH}", + ], + encoding="utf-8", + input=code, + ) + + def format_file( filename: str, - black_mode: black.FileMode, skip_errors: bool, ) -> int: with open(filename, encoding="UTF-8") as f: contents = f.read() - new_contents, errors = format_str(contents, black_mode) + new_contents, errors = format_str(contents) for error in errors: lineno = contents[: error.offset].count("\n") + 1 print(f"{filename}:{lineno}: code block parse error {error.exc}") @@ -217,15 +148,6 @@ def main(argv: Sequence[str] | None = None) -> int: type=int, default=DEFAULT_LINE_LENGTH, ) - parser.add_argument( - "-t", - "--target-version", - action="append", - type=lambda v: TargetVersion[v.upper()], - default=[], - help=f"choices: {[v.name.lower() for v in TargetVersion]}", - dest="target_versions", - ) parser.add_argument( "-S", "--skip-string-normalization", @@ -235,15 +157,9 @@ def main(argv: Sequence[str] | None = None) -> int: parser.add_argument("filenames", nargs="*") args = parser.parse_args(argv) - black_mode = black.FileMode( - target_versions=set(args.target_versions), - line_length=args.line_length, - string_normalization=not args.skip_string_normalization, - ) - retv = 0 for filename in args.filenames: - retv |= format_file(filename, black_mode, skip_errors=args.skip_errors) + retv |= format_file(filename, skip_errors=args.skip_errors) return retv diff --git a/pyproject.toml b/pyproject.toml index a9860b29ef..f1d1a66fef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,7 +51,6 @@ managed = true dev-dependencies = [ "pyright", "mypy", - "black", "respx", "pytest", "pytest-asyncio", @@ -67,17 +66,18 @@ dev-dependencies = [ [tool.rye.scripts] format = { chain = [ - "format:black", - "format:docs", "format:ruff", + "format:docs", + "fix:ruff", "format:isort", ]} "format:black" = "black ." -"format:docs" = "python bin/blacken-docs.py README.md api.md" -"format:ruff" = "ruff --fix ." +"format:docs" = "python bin/ruffen-docs.py README.md api.md" +"format:ruff" = "ruff format" "format:isort" = "isort ." "check:ruff" = "ruff ." +"fix:ruff" = "ruff --fix ." typecheck = { chain = [ "typecheck:pyright", @@ -163,6 +163,8 @@ unfixable = [ ] ignore-init-module-imports = true +[tool.ruff.format] +docstring-code-format = true [tool.ruff.per-file-ignores] "bin/**.py" = ["T201", "T203"] diff --git a/requirements-dev.lock b/requirements-dev.lock index 3e480ada33..53763f2aa9 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -13,11 +13,9 @@ argcomplete==3.1.2 attrs==23.1.0 azure-core==1.29.6 azure-identity==1.15.0 -black==23.3.0 certifi==2023.7.22 cffi==1.16.0 charset-normalizer==3.3.2 -click==8.1.7 colorlog==6.7.0 cryptography==41.0.7 dirty-equals==0.6.0 @@ -42,7 +40,6 @@ numpy==1.26.2 packaging==23.2 pandas==2.1.4 pandas-stubs==2.1.4.231218 -pathspec==0.11.2 platformdirs==3.11.0 pluggy==1.3.0 portalocker==2.8.2 @@ -58,7 +55,7 @@ python-dateutil==2.8.2 pytz==2023.3.post1 requests==2.31.0 respx==0.20.2 -ruff==0.1.7 +ruff==0.1.9 six==1.16.0 sniffio==1.3.0 time-machine==2.9.0 diff --git a/src/openai/_models.py b/src/openai/_models.py index 5b8c96010f..330a2064d8 100644 --- a/src/openai/_models.py +++ b/src/openai/_models.py @@ -382,7 +382,7 @@ class RootModel(GenericModel, Generic[_T]): For example: ```py - validated = RootModel[int](__root__='5').__root__ + validated = RootModel[int](__root__="5").__root__ # validated: 5 ``` """ diff --git a/src/openai/_types.py b/src/openai/_types.py index a20a4b4c1b..fc26d5458a 100644 --- a/src/openai/_types.py +++ b/src/openai/_types.py @@ -278,11 +278,13 @@ class NotGiven: For example: ```py - def get(timeout: Union[int, NotGiven, None] = NotGiven()) -> Response: ... + def get(timeout: Union[int, NotGiven, None] = NotGiven()) -> Response: + ... + - get(timeout=1) # 1s timeout - get(timeout=None) # No timeout - get() # Default timeout behavior, which may not be statically known at the method definition. + get(timeout=1) # 1s timeout + get(timeout=None) # No timeout + get() # Default timeout behavior, which may not be statically known at the method definition. ``` """ @@ -304,14 +306,14 @@ class Omit: ```py # as the default `Content-Type` header is `application/json` that will be sent - client.post('/upload/files', files={'file': b'my raw file content'}) + client.post("/upload/files", files={"file": b"my raw file content"}) # you can't explicitly override the header as it has to be dynamically generated # to look something like: 'multipart/form-data; boundary=0d8382fcf5f8c3be01ca2e11002d2983' - client.post(..., headers={'Content-Type': 'multipart/form-data'}) + client.post(..., headers={"Content-Type": "multipart/form-data"}) # instead you can remove the default `application/json` header by passing Omit - client.post(..., headers={'Content-Type': Omit()}) + client.post(..., headers={"Content-Type": Omit()}) ``` """ diff --git a/src/openai/_utils/_transform.py b/src/openai/_utils/_transform.py index 9117559064..342b52416a 100644 --- a/src/openai/_utils/_transform.py +++ b/src/openai/_utils/_transform.py @@ -80,9 +80,10 @@ def transform( ```py class Params(TypedDict, total=False): - card_id: Required[Annotated[str, PropertyInfo(alias='cardID')]] + card_id: Required[Annotated[str, PropertyInfo(alias="cardID")]] - transformed = transform({'card_id': ''}, Params) + + transformed = transform({"card_id": ""}, Params) # {'cardID': ''} ``` diff --git a/src/openai/_utils/_utils.py b/src/openai/_utils/_utils.py index 993462a66b..cc624b0ce1 100644 --- a/src/openai/_utils/_utils.py +++ b/src/openai/_utils/_utils.py @@ -211,13 +211,15 @@ def required_args(*variants: Sequence[str]) -> Callable[[CallableT], CallableT]: def foo(*, a: str) -> str: ... + @overload def foo(*, b: bool) -> str: ... + # This enforces the same constraints that a static type checker would # i.e. that either a or b must be passed to the function - @required_args(['a'], ['b']) + @required_args(["a"], ["b"]) def foo(*, a: str | None = None, b: bool | None = None) -> str: ... ``` diff --git a/tests/test_transform.py b/tests/test_transform.py index 5e15385f4d..c4dffb3bb0 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -189,7 +189,9 @@ class DateDictWithRequiredAlias(TypedDict, total=False): def test_datetime_with_alias() -> None: assert transform({"required_prop": None}, DateDictWithRequiredAlias) == {"prop": None} # type: ignore[comparison-overlap] - assert transform({"required_prop": date.fromisoformat("2023-02-23")}, DateDictWithRequiredAlias) == {"prop": "2023-02-23"} # type: ignore[comparison-overlap] + assert transform({"required_prop": date.fromisoformat("2023-02-23")}, DateDictWithRequiredAlias) == { + "prop": "2023-02-23" + } # type: ignore[comparison-overlap] class MyModel(BaseModel):