Repository: koxudaxi/datamodel-code-generator

Issue:
Black 24.1.0 breaks code formatting if wrap-string-literal is set
**Describe the bug**

Black [24.1.0](https://github.com/psf/black/releases/tag/24.1.0) was just released and removes support for the deprecated `--experimental-string-processing` flag (psf/black#4096). This breaks the code in [`format.py`](https://github.com/koxudaxi/datamodel-code-generator/blob/acc6bf604b13626f22fc123d72ae08ff0a114155/datamodel_code_generator/format.py#L146) that uses this option:

```
Traceback (most recent call last):
  File ".../python3.11/site-packages/datamodel_code_generator/__main__.py", line 429, in main
    generate(
  File ".../python3.11/site-packages/datamodel_code_generator/__init__.py", line 463, in generate
    results = parser.parse()
              ^^^^^^^^^^^^^^
  File ".../python3.11/site-packages/datamodel_code_generator/parser/base.py", line 1156, in parse
    code_formatter: Optional[CodeFormatter] = CodeFormatter(
                                              ^^^^^^^^^^^^^^
  File ".../python3.11/site-packages/datamodel_code_generator/format.py", line 152, in __init__
    self.black_mode = black.FileMode(
                      ^^^^^^^^^^^^^^^
TypeError: Mode.__init__() got an unexpected keyword argument 'experimental_string_processing'
```

**Expected behavior**

No crash.

**Version:**
 - OS: Linux
 - Python version: 3.11
 - datamodel-code-generator version: 0.25.2
 - black version: 0.24.1

**Additional context**

Possible mitigation:
- add a temporary upper bound to the `black` version spec in [pyproject.toml](https://github.com/koxudaxi/datamodel-code-generator/blob/acc6bf604b13626f22fc123d72ae08ff0a114155/pyproject.toml#L54)
- same, but in user environment definitions
- use `--preview --enable-unstable-feature string_processing` instead (as suggested by the black release notes).



Discussion:
@airwoodix 
Thank you for creating the issue.
OK, we should fix it.

Resolving patch:
diff --git a/datamodel_code_generator/format.py b/datamodel_code_generator/format.py
--- a/datamodel_code_generator/format.py
+++ b/datamodel_code_generator/format.py
@@ -7,6 +7,7 @@
 from warnings import warn
 
 import black
+import black.mode
 import isort
 
 from datamodel_code_generator.util import cached_property, load_toml
@@ -131,9 +132,15 @@ def __init__(
         if wrap_string_literal is not None:
             experimental_string_processing = wrap_string_literal
         else:
-            experimental_string_processing = config.get(
-                'experimental-string-processing'
-            )
+            if black.__version__ < '24.1.0':  # type: ignore
+                experimental_string_processing = config.get(
+                    'experimental-string-processing'
+                )
+            else:
+                experimental_string_processing = config.get('preview', False) and (
+                    config.get('unstable', False)
+                    or 'string_processing' in config.get('enable-unstable-feature', [])
+                )
 
         if experimental_string_processing is not None:  # pragma: no cover
             if black.__version__.startswith('19.'):  # type: ignore
@@ -141,10 +148,16 @@ def __init__(
                     f"black doesn't support `experimental-string-processing` option"  # type: ignore
                     f' for wrapping string literal in {black.__version__}'
                 )
-            else:
+            elif black.__version__ < '24.1.0':  # type: ignore
                 black_kwargs[
                     'experimental_string_processing'
                 ] = experimental_string_processing
+            elif experimental_string_processing:
+                black_kwargs['preview'] = True
+                black_kwargs['unstable'] = config.get('unstable', False)
+                black_kwargs['enabled_features'] = {
+                    black.mode.Preview.string_processing
+                }
 
         if TYPE_CHECKING:
             self.black_mode: black.FileMode


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -21,6 +21,16 @@ jobs:
             black-version: default
             python-version: 3.8
             pydantic-version: 1.8.2
+          - os: ubuntu-latest
+            isort-version: 5.6.4
+            black-version: 24.1.0
+            python-version: 3.12
+            pydantic-version: 2.4.2
+          - os: ubuntu-latest
+            isort-version: 5.6.4
+            black-version: 23.12.1
+            python-version: 3.12
+            pydantic-version: 2.4.2
         exclude:
           - os: windows-latest
             black-version: 22.1.0
@@ -91,10 +101,10 @@ jobs:
       if: matrix.pydantic-version != 'default'
       run: |
         poetry run pip install pydantic=="${{ matrix.pydantic-version }}"
-    - name: Install Black 22.1.0
-      if: matrix.black-version == '22.1.0'
+    - name: Install Black ${{ matrix.black-version }}
+      if: matrix.black-version != 'default'
       run: |
-         poetry run pip install black=="22.1.0"
+         poetry run pip install black=="${{ matrix.black-version }}"
     - name: Lint
       if: matrix.pydantic-version == 'default'
       run: |
diff --git a/tests/parser/test_openapi.py b/tests/parser/test_openapi.py
--- a/tests/parser/test_openapi.py
+++ b/tests/parser/test_openapi.py
@@ -2,6 +2,7 @@
 from pathlib import Path
 from typing import List, Optional
 
+import black
 import pydantic
 import pytest
 from packaging import version
@@ -713,6 +714,10 @@ def test_openapi_parser_responses_with_tag():
     )
 
 
+@pytest.mark.skipif(
+    black.__version__.split('.')[0] >= '24',
+    reason="Installed black doesn't support the old style",
+)
 def test_openapi_parser_with_query_parameters():
     parser = OpenAPIParser(
         data_model_field_type=DataModelFieldBase,
diff --git a/tests/test_main.py b/tests/test_main.py
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -703,6 +703,10 @@ def test_main_custom_template_dir(capsys: CaptureFixture) -> None:
     assert captured.err == inferred_message.format('openapi') + '\n'
 
 
+@pytest.mark.skipif(
+    black.__version__.split('.')[0] >= '24',
+    reason="Installed black doesn't support the old style",
+)
 @freeze_time('2019-07-26')
 def test_pyproject():
     if platform.system() == 'Windows':
@@ -1755,6 +1759,10 @@ def test_main_use_standard_collections(tmpdir_factory: TempdirFactory) -> None:
         assert result == path.read_text()
 
 
+@pytest.mark.skipif(
+    black.__version__.split('.')[0] >= '24',
+    reason="Installed black doesn't support the old style",
+)
 def test_main_use_generic_container_types(tmpdir_factory: TempdirFactory) -> None:
     output_directory = Path(tmpdir_factory.mktemp('output'))
 
@@ -1781,6 +1789,10 @@ def test_main_use_generic_container_types(tmpdir_factory: TempdirFactory) -> Non
         assert result == path.read_text()
 
 
+@pytest.mark.skipif(
+    black.__version__.split('.')[0] >= '24',
+    reason="Installed black doesn't support the old style",
+)
 @pytest.mark.benchmark
 def test_main_use_generic_container_types_standard_collections(
     tmpdir_factory: TempdirFactory,
@@ -2366,6 +2378,10 @@ def test_main_openapi_use_one_literal_as_default():
     version.parse(pydantic.VERSION) < version.parse('1.9.0'),
     reason='Require Pydantic version 1.9.0 or later ',
 )
+@pytest.mark.skipif(
+    black.__version__.split('.')[0] >= '24',
+    reason="Installed black doesn't support the old style",
+)
 @freeze_time('2019-07-26')
 def test_main_openapi_enum_models_as_literal_all():
     with TemporaryDirectory() as output_dir:
@@ -2397,6 +2413,10 @@ def test_main_openapi_enum_models_as_literal_all():
     version.parse(pydantic.VERSION) < version.parse('1.9.0'),
     reason='Require Pydantic version 1.9.0 or later ',
 )
+@pytest.mark.skipif(
+    black.__version__.split('.')[0] >= '24',
+    reason="Installed black doesn't support the old style",
+)
 @freeze_time('2019-07-26')
 def test_main_openapi_enum_models_as_literal_py37(capsys):
     with TemporaryDirectory() as output_dir:
@@ -2687,6 +2707,10 @@ def test_main_all_of_with_object():
             )
 
 
+@pytest.mark.skipif(
+    black.__version__.split('.')[0] >= '24',
+    reason="Installed black doesn't support the old style",
+)
 @freeze_time('2019-07-26')
 def test_main_combined_array():
     with TemporaryDirectory() as output_dir:
@@ -3359,6 +3383,10 @@ def test_main_strict_types():
         )
 
 
+@pytest.mark.skipif(
+    black.__version__.split('.')[0] >= '24',
+    reason="Installed black doesn't support the old style",
+)
 @freeze_time('2019-07-26')
 def test_main_strict_types_all():
     with TemporaryDirectory() as output_dir:
diff --git a/tests/test_main_kr.py b/tests/test_main_kr.py
--- a/tests/test_main_kr.py
+++ b/tests/test_main_kr.py
@@ -3,6 +3,7 @@
 from pathlib import Path
 from tempfile import TemporaryDirectory
 
+import black
 import pytest
 from freezegun import freeze_time
 
@@ -180,6 +181,10 @@ def test_main_custom_template_dir(capsys: CaptureFixture) -> None:
     assert captured.err == inferred_message.format('openapi') + '\n'
 
 
+@pytest.mark.skipif(
+    black.__version__.split('.')[0] >= '24',
+    reason="Installed black doesn't support the old style",
+)
 @freeze_time('2019-07-26')
 def test_pyproject():
     with TemporaryDirectory() as output_dir: