Repository: lincolnloop/goodconf

Issue:
Markdown generation issues in v5
We use `goodconf` and `cog` to generate a list of config values for lincolnloop/django-layout.

After upgrading to v5, I'm seeing the following diff

```diff
63c63
<   * type: `Literal['development', 'dev', 'production']`
---
>   * type: `typing.Literal['development', 'dev', 'production']`
67c67
<   * type: `Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']`
---
>   * type: `typing.Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']`
71a72
>   * default: `PydanticUndefined`
```

You can see the expected output on the https://github.com/lincolnloop/goodconf/blob/main/README.rst. Here is the new output:

```markdown
* **DJANGO_ENV**
  * description: Toggle deployment settings for local development or production
  * type: `typing.Literal['development', 'dev', 'production']`
  * default: `production`
* **LOG_LEVEL**
  * description: Python logging level
  * type: `typing.Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']`
  * default: `INFO`
* **SECRET_KEY** _REQUIRED_
  * description: A long random string you keep secret https://docs.djangoproject.com/en/5.1/ref/settings/#secret-key
  * type: `str`
  * default: `PydanticUndefined`
```

So the issues are:

* `typing.Literal` should be `Literal` (not sure if this also applies to other types)
*  There should be no default listed for `SECRET_KEY`

Discussion:

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 076390a..0df2d34 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -3,9 +3,10 @@ Change Log
 ==========
 
 5.0.1 (Unreleased)
-==================
+========================
 
 - Support reading TOML files via ``tomllib`` on Python 3.11+
+- Update Markdown generation, so that output matches v4 output
 
 5.0.0 (13 August 2024)
 ========================
diff --git a/goodconf/__init__.py b/goodconf/__init__.py
index c2c87db..bbc8abf 100644
--- a/goodconf/__init__.py
+++ b/goodconf/__init__.py
@@ -99,6 +99,27 @@ def _find_file(filename: str, require: bool = True) -> str | None:
     return os.path.abspath(filename)
 
 
+def _fieldinfo_to_str(field_info: FieldInfo) -> str:
+    """
+    Return the string representation of a pydantic.fields.FieldInfo.
+    """
+    if isinstance(field_info.annotation, type) and not isinstance(
+        field_info.annotation, GenericAlias
+    ):
+        # For annotation like <class 'int'>, we use its name ("int").
+        field_type = field_info.annotation.__name__
+    else:
+        if str(field_info.annotation).startswith("typing."):
+            # For annotation like typing.Literal['a', 'b'], we use
+            # its string representation, but without "typing." ("Literal['a', 'b']").
+            field_type = str(field_info.annotation)[len("typing.") :]
+        else:
+            # For annotation like list[str], we use its string
+            # representation ("list[str]").
+            field_type = field_info.annotation
+    return field_type
+
+
 def initial_for_field(name: str, field_info: FieldInfo) -> Any:
     try:
         json_schema_extra = field_info.json_schema_extra or {}
@@ -296,18 +317,12 @@ class GoodConf(BaseSettings):
             if field_info.description:
                 lines.append(f"  * description: {field_info.description}")
             # We want to append a line with the field_info type, and sometimes
-            # field_info.annotation looks the way we want, like
-            # 'typing.Literal['a', 'b']', but other times, it includes some extra
-            # text, like '<class 'bool'>'. Therefore, we have some logic to make
-            # the type show up the way we want.
-            field_type = (
-                field_info.annotation.__name__
-                if isinstance(field_info.annotation, type)
-                and not isinstance(field_info.annotation, GenericAlias)
-                else field_info.annotation
-            )
+            # field_info.annotation looks the way we want, like 'list[str]', but
+            # other times, it includes some extra text, like '<class 'bool'>'.
+            # Therefore, we have some logic to make the type show up the way we want.
+            field_type = _fieldinfo_to_str(field_info)
             lines.append(f"  * type: `{field_type}`")
-            if field_info.default is not None:
+            if field_info.default not in [None, PydanticUndefined]:
                 lines.append(f"  * default: `{field_info.default}`")
         return "\n".join(lines)
 


Test patch:
diff --git a/tests/test_goodconf.py b/tests/test_goodconf.py
index 0cf9daa..4c7c76d 100644
--- a/tests/test_goodconf.py
+++ b/tests/test_goodconf.py
@@ -151,10 +151,12 @@ def test_generate_markdown_types():
     class TestConf(GoodConf):
         a: Literal["a", "b"] = Field(default="a")
         b: list[str] = Field()
+        c: None
 
     lines = TestConf.generate_markdown().splitlines()
-    assert "  * type: `typing.Literal['a', 'b']`" in lines
+    assert "  * type: `Literal['a', 'b']`" in lines
     assert "  * type: `list[str]`" in lines
+    assert "default: `PydanticUndefined`" not in str(lines)
 
 
 def test_generate_markdown_required():