Repository: jdkandersson/OpenAlchemy

Issue:
change black to be a facade
As a developer I want to ring fence the black dependency so that it can be replaced easily in the future.

- [x] Define facade interface
- [x] Integrate with black
- [x]  move black dependencies

Comments:

Patch:
diff --git a/open_alchemy/facades/__init__.py b/open_alchemy/facades/__init__.py
index 76fedf12..6b87c8e0 100644
--- a/open_alchemy/facades/__init__.py
+++ b/open_alchemy/facades/__init__.py
@@ -1,6 +1,7 @@
 """Facades for ringfenced modules."""
 # pylint: disable=useless-import-alias
 
+from . import code_formatter as code_formatter
 from . import jsonschema as jsonschema
 from . import models as models
 from . import sqlalchemy as sqlalchemy
diff --git a/open_alchemy/facades/code_formatter/__init__.py b/open_alchemy/facades/code_formatter/__init__.py
new file mode 100644
index 00000000..bcb579d0
--- /dev/null
+++ b/open_alchemy/facades/code_formatter/__init__.py
@@ -0,0 +1,22 @@
+"""Apply automatic code formatting to source code."""
+
+import black
+
+
+def apply(*, source: str) -> str:
+    """
+    Apply automatic code formatting to source code.
+
+    Args:
+        source: The source code.
+
+    Returns:
+        The formatted source code.
+
+    """
+    try:
+        return black.format_file_contents(
+            src_contents=source, fast=False, mode=black.FileMode()
+        )
+    except black.NothingChanged:
+        return source
diff --git a/open_alchemy/models_file/__init__.py b/open_alchemy/models_file/__init__.py
index 6075191e..53a6ee33 100644
--- a/open_alchemy/models_file/__init__.py
+++ b/open_alchemy/models_file/__init__.py
@@ -4,8 +4,7 @@
 import dataclasses
 import typing
 
-import black
-
+from open_alchemy import facades
 from open_alchemy import types as oa_types
 
 from . import model as _model
@@ -49,9 +48,4 @@ class ModelsFile:
 
         # Generate source code for models file
         raw_source = _models.generate(models=model_sources)
-        try:
-            return black.format_file_contents(
-                src_contents=raw_source, fast=False, mode=black.FileMode()
-            )
-        except black.NothingChanged:
-            return raw_source
+        return facades.code_formatter.apply(source=raw_source)
diff --git a/setup.cfg b/setup.cfg
index ecfa1cad..9aeea833 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -19,6 +19,7 @@ markers =
     only_this
     example
     init
+    code_formatter
 python_functions = test_*
 mocked-sessions = examples.app.database.db.session
 


Test patch:
diff --git a/tests/open_alchemy/facades/test_code_formatter.py b/tests/open_alchemy/facades/test_code_formatter.py
new file mode 100644
index 00000000..e881ef8c
--- /dev/null
+++ b/tests/open_alchemy/facades/test_code_formatter.py
@@ -0,0 +1,25 @@
+"""Tests for code formatting."""
+
+import pytest
+
+from open_alchemy import facades
+
+
+@pytest.mark.parametrize(
+    "source, expected_source",
+    [
+        pytest.param("'test'\n", '"test"\n', id="apply formatting"),
+        pytest.param('"test"\n', '"test"\n', id="no formatting"),
+    ],
+)
+@pytest.mark.facade
+@pytest.mark.code_formatter
+def test_apply(source, expected_source):
+    """
+    GIVEN source code and expected source code
+    WHEN apply is called with the source code
+    THEN the expected source code is returned.
+    """
+    returned_source = facades.code_formatter.apply(source=source)
+
+    assert returned_source == expected_source
diff --git a/tests/open_alchemy/models_file/test_integration.py b/tests/open_alchemy/models_file/test_integration.py
index 976b440e..c131ca75 100644
--- a/tests/open_alchemy/models_file/test_integration.py
+++ b/tests/open_alchemy/models_file/test_integration.py
@@ -8,7 +8,6 @@ from mypy import api
 from open_alchemy import models_file
 
 _DOCSTRING = '"""Autogenerated SQLAlchemy models based on OpenAlchemy models."""'
-_LONG_NAME = "extremely_long_name_that_will_cause_wrapping_aaaaaaaaaaaaaaaaa"
 _EXPECTED_TD_BASE = "typing.TypedDict"
 if sys.version_info[1] < 8:
     _EXPECTED_TD_BASE = "typing_extensions.TypedDict"
@@ -116,110 +115,6 @@ class TModel({_EXPECTED_MODEL_BASE}):
         ...
 
 
-Model: typing.Type[TModel] = models.Model  # type: ignore
-''',
-        ),
-        (
-            [
-                (
-                    {
-                        "properties": {
-                            "ref_model": {"type": "object", "x-de-$ref": "RefModel"}
-                        }
-                    },
-                    "Model",
-                )
-            ],
-            f'''{_DOCSTRING}
-# pylint: disable=no-member,super-init-not-called,unused-argument
-
-import typing
-
-import sqlalchemy{_ADDITIONAL_IMPORT}
-from sqlalchemy import orm
-
-from open_alchemy import models
-
-
-class ModelDict({_EXPECTED_TD_BASE}, total=False):
-    """TypedDict for properties that are not required."""
-
-    ref_model: typing.Optional["RefModelDict"]
-
-
-class TModel({_EXPECTED_MODEL_BASE}):
-    """
-    SQLAlchemy model protocol.
-
-    Attrs:
-        ref_model: The ref_model of the Model.
-
-    """
-
-    # SQLAlchemy properties
-    __table__: sqlalchemy.Table
-    __tablename__: str
-    query: orm.Query
-
-    # Model properties
-    ref_model: 'sqlalchemy.Column[typing.Optional["TRefModel"]]'
-
-    def __init__(self, ref_model: typing.Optional["TRefModel"] = None) -> None:
-        """
-        Construct.
-
-        Args:
-            ref_model: The ref_model of the Model.
-
-        """
-        ...
-
-    @classmethod
-    def from_dict(cls, ref_model: typing.Optional["RefModelDict"] = None) -> "TModel":
-        """
-        Construct from a dictionary (eg. a POST payload).
-
-        Args:
-            ref_model: The ref_model of the Model.
-
-        Returns:
-            Model instance based on the dictionary.
-
-        """
-        ...
-
-    @classmethod
-    def from_str(cls, value: str) -> "TModel":
-        """
-        Construct from a JSON string (eg. a POST payload).
-
-        Returns:
-            Model instance based on the JSON string.
-
-        """
-        ...
-
-    def to_dict(self) -> ModelDict:
-        """
-        Convert to a dictionary (eg. to send back for a GET request).
-
-        Returns:
-            Dictionary based on the model instance.
-
-        """
-        ...
-
-    def to_str(self) -> str:
-        """
-        Convert to a JSON string (eg. to send back for a GET request).
-
-        Returns:
-            JSON string based on the model instance.
-
-        """
-        ...
-
-
 Model: typing.Type[TModel] = models.Model  # type: ignore
 ''',
         ),
@@ -401,121 +296,10 @@ class TModel2({_EXPECTED_MODEL_BASE}):
 
 
 Model2: typing.Type[TModel2] = models.Model2  # type: ignore
-''',
-        ),
-        (
-            [({"properties": {_LONG_NAME: {"type": "integer"}}}, "Model")],
-            f'''{_DOCSTRING}
-# pylint: disable=no-member,super-init-not-called,unused-argument
-
-import typing
-
-import sqlalchemy{_ADDITIONAL_IMPORT}
-from sqlalchemy import orm
-
-from open_alchemy import models
-
-
-class ModelDict({_EXPECTED_TD_BASE}, total=False):
-    """TypedDict for properties that are not required."""
-
-    {_LONG_NAME}: typing.Optional[int]
-
-
-class TModel({_EXPECTED_MODEL_BASE}):
-    """
-    SQLAlchemy model protocol.
-
-    Attrs:
-        {_LONG_NAME}: The
-            {_LONG_NAME} of
-            the Model.
-
-    """
-
-    # SQLAlchemy properties
-    __table__: sqlalchemy.Table
-    __tablename__: str
-    query: orm.Query
-
-    # Model properties
-    {_LONG_NAME}: "sqlalchemy.Column[typing.Optional[int]]"
-
-    def __init__(
-        self,
-        {_LONG_NAME}: typing.Optional[
-            int
-        ] = None,
-    ) -> None:
-        """
-        Construct.
-
-        Args:
-            {_LONG_NAME}: The
-                {_LONG_NAME}
-                of the Model.
-
-        """
-        ...
-
-    @classmethod
-    def from_dict(
-        cls,
-        {_LONG_NAME}: typing.Optional[
-            int
-        ] = None,
-    ) -> "TModel":
-        """
-        Construct from a dictionary (eg. a POST payload).
-
-        Args:
-            {_LONG_NAME}: The
-                {_LONG_NAME}
-                of the Model.
-
-        Returns:
-            Model instance based on the dictionary.
-
-        """
-        ...
-
-    @classmethod
-    def from_str(cls, value: str) -> "TModel":
-        """
-        Construct from a JSON string (eg. a POST payload).
-
-        Returns:
-            Model instance based on the JSON string.
-
-        """
-        ...
-
-    def to_dict(self) -> ModelDict:
-        """
-        Convert to a dictionary (eg. to send back for a GET request).
-
-        Returns:
-            Dictionary based on the model instance.
-
-        """
-        ...
-
-    def to_str(self) -> str:
-        """
-        Convert to a JSON string (eg. to send back for a GET request).
-
-        Returns:
-            JSON string based on the model instance.
-
-        """
-        ...
-
-
-Model: typing.Type[TModel] = models.Model  # type: ignore
 ''',
         ),
     ],
-    ids=["single", "single object", "multiple", "black"],
+    ids=["single", "multiple"],
 )
 @pytest.mark.models_file
 def test_integration(schemas, expected_source):