Commit e1b60b22

Stainless Bot <107565488+stainless-bot@users.noreply.github.com>
2023-12-22 18:45:53
chore(internal): use ruff instead of black for formatting (#1008)
1 parent ce04ec2
bin/blacken-docs.py → 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<before>^(?P<indent> *)```\s*python\n)" r"(?P<code>.*?)" r"(?P<after>^(?P=indent)```\s*$)",
     re.DOTALL | re.MULTILINE,
@@ -19,55 +17,12 @@ MD_PYCON_RE = re.compile(
     r"(?P<before>^(?P<indent> *)```\s*pycon\n)" r"(?P<code>.*?)" r"(?P<after>^(?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<before>"
-    rf"^(?P<indent> *)\.\. ("
-    rf"jupyter-execute::|"
-    rf"{BLOCK_TYPES}:: (?P<lang>\w+)|"
-    rf"{DOCTEST_TYPES}::.*"
-    rf")\n"
-    rf"((?P=indent) +:.*\n)*"
-    rf"\n*"
-    rf")"
-    rf"(?P<code>(^((?P=indent) +.*)?\n)+)",
-    re.MULTILINE,
-)
-RST_PYCON_RE = re.compile(
-    r"(?P<before>"
-    r"(?P<indent> *)\.\. ((code|code-block):: pycon|doctest::.*)\n"
-    r"((?P=indent) +:.*\n)*"
-    r"\n*"
-    r")"
-    r"(?P<code>(^((?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<before>^(?P<indent> *)\\begin{minted}{python}\n)"
-    r"(?P<code>.*?)"
-    r"(?P<after>^(?P=indent)\\end{minted}\s*$)",
-    re.DOTALL | re.MULTILINE,
-)
-LATEX_PYCON_RE = re.compile(
-    r"(?P<before>^(?P<indent> *)\\begin{minted}{pycon}\n)" r"(?P<code>.*?)" r"(?P<after>^(?P=indent)\\end{minted}\s*$)",
-    re.DOTALL | re.MULTILINE,
-)
-PYTHONTEX_LANG = r"(?P<lang>pyblock|pycode|pyconsole|pyverbatim)"
-PYTHONTEX_RE = re.compile(
-    rf"(?P<before>^(?P<indent> *)\\begin{{{PYTHONTEX_LANG}}}\n)"
-    rf"(?P<code>.*?)"
-    rf"(?P<after>^(?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 format_str(
     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 format_str(
 
             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 format_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
 
 
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': '<my card ID>'}, Params)
+
+    transformed = transform({"card_id": "<my card ID>"}, Params)
     # {'cardID': '<my card ID>'}
     ```
 
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:
         ...
     ```
src/openai/_models.py
@@ -382,7 +382,7 @@ elif not TYPE_CHECKING:  # TODO: condition is weird
 
         For example:
         ```py
-        validated = RootModel[int](__root__='5').__root__
+        validated = RootModel[int](__root__="5").__root__
         # validated: 5
         ```
         """
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()})
     ```
     """
 
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):
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"]
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