Repository: mirumee/ariadne-codegen (python)

Issue:
Allow configuration in pyproject.toml under `tool.argiadne-codegen`
First of all, thanks for this great project!

AFAIK, the official spec recommends that additional config for specific tools is tracked within the tools table of the pyproject.toml file (i.e. `tool.argiadne-codegen`) . https://peps.python.org/pep-0518/#tool-table. 

While the its more of a nuisance then a real problem, right now my linters are complaining about an unknown key in the file.

If you think its worthwhile to change this, I'd be happy to submit a PR to support either both keys, or change the current behaviour.

Pull request:
Add support for [tool.ariadne-codegen] key. Deprecate [ariadne-codegen].
resolves #84 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9094554..bdec921 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@
 - Added `InitFileGenerator`, `EnumsGenerator`, `ClientGenerator` and `ArgumentsGenerator` plugin hooks.
 - Added `InputTypesGenerator` and `ResultTypesGenerator` plugin hooks.
 - Added `ScalarsDefinitionsGenerator` and `PackageGenerator` plugin hooks.
+- Added support for `[tool.ariadne-codegen]` section key. Deprecated `[ariadne-codegen]`.
 
 
 ## 0.3.0 (2023-02-21)
diff --git a/EXAMPLE.md b/EXAMPLE.md
index 1f8885f..c8d7034 100644
--- a/EXAMPLE.md
+++ b/EXAMPLE.md
@@ -113,10 +113,10 @@ fragment UserPersonalData on User {
 
 ## Running
 
-Add `[ariadne-codegen]` section to `pyproject.toml` with paths to files with [schema](#schema-file) and [queries/mutations](#queriesmutations-file).
+Add `[tool.ariadne-codegen]` section to `pyproject.toml` with paths to files with [schema](#schema-file) and [queries/mutations](#queriesmutations-file).
 
 ```toml
-[ariadne-codegen]
+[tool.ariadne-codegen]
 schema_path = "schema.graphql" 
 queries_path = "queries.graphql"
 ```
diff --git a/PLUGINS.md b/PLUGINS.md
index f353f8b..affd33a 100644
--- a/PLUGINS.md
+++ b/PLUGINS.md
@@ -314,6 +314,6 @@ class VersionPlugin(Plugin):
 `VersionPlugin` reads version from parsed `pyproject.toml`, eg. following entry will produce `__version__ = "0.21"`.
 
 ```toml
-[tools.version_plugin]
+[tool.version_plugin]
 version = 0.21
 ```
diff --git a/README.md b/README.md
index facbf5a..bbf20ae 100644
--- a/README.md
+++ b/README.md
@@ -33,7 +33,7 @@ $ pip install ariadne-codegen
 
 ## Configuration
 
-`ariadne-codegen` reads configuration from `[ariadne-codegen]` section in your `pyproject.toml`'.
+`ariadne-codegen` reads configuration from `[tool.ariadne-codegen]` section in your `pyproject.toml`'.
 
 Required settings:
 
@@ -97,7 +97,7 @@ By default, not built-in scalars are represented as `typing.Any` in generated cl
 You can provide information about specific scalar by adding section to `pyproject.toml`:
 
 ```toml
-[ariadne-codegen.scalars.{graphql scalar name}]
+[tool.ariadne-codegen.scalars.{graphql scalar name}]
 type = "(required) python type name"
 serialize = "function used to serialize scalar"
 parse = "function used to create scalar instance from serialized form"
@@ -112,7 +112,7 @@ All occurences of `{graphql scalar name}` will be represented as `type`. If prov
 In this case scalar is mapped to built-in `str` which doesn\`t require custom `serialize ` and `parse` methods. 
 
 ```toml
-[ariadne-codegen.scalars.SCALARA]
+[tool.ariadne-codegen.scalars.SCALARA]
 type = "str"
 ```
 
@@ -122,7 +122,7 @@ type = "str"
 In this scenario scalar is represented as `datetime`, so it needs to be imported. Pydantic handles serialization and deserialization so custom `parse` and `serialize` is not necessary.
 
 ```toml
-[ariadne-codegen.scalars.DATETIME]
+[tool.ariadne-codegen.scalars.DATETIME]
 type = "datetime"
 import = "datetime"
 ```
@@ -133,11 +133,11 @@ import = "datetime"
 In this example scalar is represented as class `TypeB`. Pydantic can\`t handle  serialization and deserialization so custom `parse` and `serialize` is necessary. To provide `type`, `parse` and `serialize` implementation we can use `files_to_include` to copy `type_b.py` file.
 
 ```toml
-[ariadne-codegen]
+[tool.ariadne-codegen]
 ...
 files_to_include = [".../type_b.py"]
 
-[ariadne-codegen.scalars.SCALARB]
+[tool.ariadne-codegen.scalars.SCALARB]
 type = "TypeB"
 parse = "parse_b"
 serialize = "serialize_b"
diff --git a/ariadne_codegen/config.py b/ariadne_codegen/config.py
index 6794853..ace920e 100644
--- a/ariadne_codegen/config.py
+++ b/ariadne_codegen/config.py
@@ -3,12 +3,15 @@ from keyword import iskeyword
 from pathlib import Path
 from textwrap import dedent
 from typing import Dict, List, Optional
+from warnings import simplefilter, warn
 
 import toml
 
 from .exceptions import ConfigFileNotFound, InvalidConfiguration, MissingConfiguration
 from .generators.scalars import ScalarData
 
+simplefilter("default", DeprecationWarning)
+
 
 @dataclass
 class Settings:
@@ -115,16 +118,11 @@ def get_config_dict() -> Dict:
     return toml.load(config_path)
 
 
-def parse_config_dict(
-    config_dict: Dict, section_key: str = "ariadne-codegen"
-) -> Settings:
+def parse_config_dict(config_dict: Dict) -> Settings:
     """
     Parse configuration from toml file. Raise exception if section or key was not found.
     """
-    if section_key not in config_dict:
-        raise MissingConfiguration(f"Config has no [{section_key}] section.")
-    section = config_dict[section_key]
-
+    section = get_section(config_dict)
     try:
         section["scalars"] = {
             name: ScalarData(
@@ -144,12 +142,32 @@ def parse_config_dict(
         return Settings(**section)
     except TypeError as exc:
         expected_fields = {f.name for f in fields(Settings)}
-        missing_fields = expected_fields.difference(config_dict[section_key])
+        missing_fields = expected_fields.difference(section)
         raise MissingConfiguration(
             f"Missing configuration fields: {', '.join(missing_fields)}"
         ) from exc
 
 
+def get_section(config_dict: Dict) -> Dict:
+    """Get section from config dict."""
+    tool_key = "tool"
+    codegen_key = "ariadne-codegen"
+    if tool_key in config_dict and codegen_key in config_dict.get(tool_key, {}):
+        return config_dict[tool_key][codegen_key]
+
+    if codegen_key in config_dict:
+        warn(
+            f"Support for [{codegen_key}] section has been deprecated "
+            "and will be dropped in future release. "
+            f"Instead use [{tool_key}.{codegen_key}].",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return config_dict[codegen_key]
+
+    raise MissingConfiguration(f"Config has no [{tool_key}.{codegen_key}] section.")
+
+
 def get_used_settings_message(settings: Settings) -> str:
     comments_msg = (
         "Including comments."
diff --git a/pyproject.toml b/pyproject.toml
index 3448d0c..60d49e6 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -82,7 +82,3 @@ include = [
 exclude = [
   "tests",
 ]
-
-[ariadne-codegen]
-schema_path = "."
-queries_path = "."


Test patch:
diff --git a/tests/main/custom_base_client/pyproject.toml b/tests/main/custom_base_client/pyproject.toml
index 708fc01..94d7134 100644
--- a/tests/main/custom_base_client/pyproject.toml
+++ b/tests/main/custom_base_client/pyproject.toml
@@ -1,4 +1,4 @@
-[ariadne-codegen]
+[tool.ariadne-codegen]
 schema_path = "schema.graphql"
 queries_path = "queries.graphql"
 include_comments = false
diff --git a/tests/main/custom_files_names/pyproject.toml b/tests/main/custom_files_names/pyproject.toml
index baf1faf..b60d535 100644
--- a/tests/main/custom_files_names/pyproject.toml
+++ b/tests/main/custom_files_names/pyproject.toml
@@ -1,4 +1,4 @@
-[ariadne-codegen]
+[tool.ariadne-codegen]
 schema_path = "schema.graphql"
 queries_path = "queries.graphql"
 include_comments = false
diff --git a/tests/main/custom_scalars/pyproject.toml b/tests/main/custom_scalars/pyproject.toml
index 4f8e26f..fe7d539 100644
--- a/tests/main/custom_scalars/pyproject.toml
+++ b/tests/main/custom_scalars/pyproject.toml
@@ -1,18 +1,18 @@
-[ariadne-codegen]
+[tool.ariadne-codegen]
 schema_path = "schema.graphql"
 queries_path = "queries.graphql"
 include_comments = false
 target_package_name = "custom_scalars_client"
 files_to_include = ["custom_scalars.py"]
 
-[ariadne-codegen.scalars.CUSTOMID]
+[tool.ariadne-codegen.scalars.CUSTOMID]
 type = "int"
 
-[ariadne-codegen.scalars.DATETIME]
+[tool.ariadne-codegen.scalars.DATETIME]
 type = "datetime"
 import = "datetime"
 
-[ariadne-codegen.scalars.CODE]
+[tool.ariadne-codegen.scalars.CODE]
 type = "Code"
 serialize = "serialize_code"
 parse = "parse_code"
diff --git a/tests/main/example/pyproject.toml b/tests/main/example/pyproject.toml
index 36263ee..cbc0a60 100644
--- a/tests/main/example/pyproject.toml
+++ b/tests/main/example/pyproject.toml
@@ -1,4 +1,4 @@
-[ariadne-codegen]
+[tool.ariadne-codegen]
 schema_path = "schema.graphql"
 queries_path = "queries.graphql"
 include_comments = false
diff --git a/tests/main/extended_models/pyproject.toml b/tests/main/extended_models/pyproject.toml
index de871d9..fcf7ed7 100644
--- a/tests/main/extended_models/pyproject.toml
+++ b/tests/main/extended_models/pyproject.toml
@@ -1,4 +1,4 @@
-[ariadne-codegen]
+[tool.ariadne-codegen]
 schema_path = "schema.graphql"
 queries_path = "queries.graphql"
 include_comments = false
diff --git a/tests/main/inline_fragments/pyproject.toml b/tests/main/inline_fragments/pyproject.toml
index 6223998..9de607c 100644
--- a/tests/main/inline_fragments/pyproject.toml
+++ b/tests/main/inline_fragments/pyproject.toml
@@ -1,4 +1,4 @@
-[ariadne-codegen]
+[tool.ariadne-codegen]
 schema_path = "schema.graphql"
 queries_path = "queries.graphql"
 include_comments = false
diff --git a/tests/main/invalid_pyprojects/no_files.toml b/tests/main/invalid_pyprojects/no_files.toml
index 36263ee..cbc0a60 100644
--- a/tests/main/invalid_pyprojects/no_files.toml
+++ b/tests/main/invalid_pyprojects/no_files.toml
@@ -1,4 +1,4 @@
-[ariadne-codegen]
+[tool.ariadne-codegen]
 schema_path = "schema.graphql"
 queries_path = "queries.graphql"
 include_comments = false
diff --git a/tests/main/invalid_pyprojects/operation_not_valid_for_schema.toml b/tests/main/invalid_pyprojects/operation_not_valid_for_schema.toml
index 61556e4..b73465e 100644
--- a/tests/main/invalid_pyprojects/operation_not_valid_for_schema.toml
+++ b/tests/main/invalid_pyprojects/operation_not_valid_for_schema.toml
@@ -1,4 +1,4 @@
-[ariadne-codegen]
+[tool.ariadne-codegen]
 schema_path = "schema.graphql"
 queries_path = "not_valid_query.graphql"
 include_comments = false
diff --git a/tests/main/remote_schema/pyproject.toml b/tests/main/remote_schema/pyproject.toml
index def2582..da20edd 100644
--- a/tests/main/remote_schema/pyproject.toml
+++ b/tests/main/remote_schema/pyproject.toml
@@ -1,4 +1,4 @@
-[ariadne-codegen]
+[tool.ariadne-codegen]
 queries_path = "queries.graphql"
 target_package_name = "remote_schema_client"
 include_comments = false
diff --git a/tests/test_config.py b/tests/test_config.py
index 4ca672f..8e73872 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -6,6 +6,7 @@ from ariadne_codegen.config import (
     Settings,
     get_config_dict,
     get_config_file_path,
+    get_section,
     parse_config_dict,
 )
 from ariadne_codegen.exceptions import ConfigFileNotFound, MissingConfiguration
@@ -20,7 +21,7 @@ def config_file(tmp_path_factory):
     queries_path = file_.parent.joinpath("queries")
     queries_path.mkdir()
     config = """
-        [ariadne-codegen]
+        [tool.ariadne-codegen]
         schema_path = "schema.graphql"
         queries_path = "queries"
     """
@@ -65,17 +66,19 @@ def test_parse_config_dict_returns_settings_object(tmp_path):
     queries_path = tmp_path / "queries.graphql"
     queries_path.touch()
     config_dict = {
-        "ariadne-codegen": {
-            "schema_path": schema_path.as_posix(),
-            "queries_path": queries_path.as_posix(),
-            "scalars": {
-                "ID": {
-                    "type": "str",
-                    "parse": "parse_id",
-                    "serialize": "serialize_id",
-                    "import": ".custom_scalars",
-                }
-            },
+        "tool": {
+            "ariadne-codegen": {
+                "schema_path": schema_path.as_posix(),
+                "queries_path": queries_path.as_posix(),
+                "scalars": {
+                    "ID": {
+                        "type": "str",
+                        "parse": "parse_id",
+                        "serialize": "serialize_id",
+                        "import": ".custom_scalars",
+                    }
+                },
+            }
         }
     }
     settings = parse_config_dict(config_dict)
@@ -101,7 +104,7 @@ def test_parse_config_dict_without_section_raises_missing_configuration_exceptio
 
 
 def test_parse_config_dict_without_field_raises_missing_configuration_exception():
-    config_dict = {"ariadne-codegen": {"invalid_field": "."}}
+    config_dict = {"tool": {"ariadne-codegen": {"invalid_field": "."}}}
 
     with pytest.raises(MissingConfiguration):
         parse_config_dict(config_dict)
@@ -115,12 +118,31 @@ def test_parse_config_dict_with_invalid_scalar_section_raises_missing_configurat
     queries_path = tmp_path / "queries.graphql"
     queries_path.touch()
     config_dict = {
-        "ariadne-codegen": {
-            "schema_path": schema_path.as_posix(),
-            "queries_path": queries_path.as_posix(),
-            "scalars": {"ID": {"invalid_key": "str"}},
+        "tool": {
+            "ariadne-codegen": {
+                "schema_path": schema_path.as_posix(),
+                "queries_path": queries_path.as_posix(),
+                "scalars": {"ID": {"invalid_key": "str"}},
+            }
         }
     }
 
     with pytest.raises(MissingConfiguration):
         parse_config_dict(config_dict)
+
+
+def test_get_section_returns_correct_dictionary_from_tool_key():
+    section_dict = {"schema_path": ".", "queries_path": "."}
+    config_dict = {"tool": {"ariadne-codegen": section_dict}}
+
+    assert get_section(config_dict) == section_dict
+
+
+def test_get_section_returns_dict_from_codegen_key_and_raises_deprecation_warning():
+    section_dict = {"schema_path": ".", "queries_path": "."}
+    config_dict = {"ariadne-codegen": section_dict}
+
+    with pytest.deprecated_call():
+        result = get_section(config_dict)
+
+    assert result == section_dict