Repository: danielgtaylor/python-betterproto

Issue:
pydantic option broken
### Summary

Instantiating messages generated with pydantic option throws exceptions.

### Protobuf schema

```proto
syntax = "proto3";

package prototest.v1;

message One {
  uint32 message = 1;
}

message Two {
  string message = 1;
}

message Message {
  oneof message {
    One one = 1;
    Two two = 2;
  }
}
```


### Reproduction Steps

- install main branch betterproto
- generate code with the `pydantic_dataclasses` option enabled
- import your generated code
- instantiate a message object

### Expected Results

No exception

### Actual Results

```python
>>> Message()
Traceback (most recent call last):
  File "/home/bb/.cache/pypoetry/virtualenvs/testproto-ZoH_3K0Y-py3.12/lib/python3.12/site-packages/betterproto/__init__.py", line 901, in _betterproto
    return cls._betterproto_meta
           ^^^^^^^^^^^^^^^^^^^^^
AttributeError: type object 'Message' has no attribute '_betterproto_meta'. Did you mean: '_betterproto'?

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/bb/.cache/pypoetry/virtualenvs/testproto-ZoH_3K0Y-py3.12/lib/python3.12/site-packages/pydantic/_internal/_dataclasses.py", line 141, in __init__
    s.__pydantic_validator__.validate_python(ArgsKwargs(args, kwargs), self_instance=s)
  File "/home/bb/.cache/pypoetry/virtualenvs/testproto-ZoH_3K0Y-py3.12/lib/python3.12/site-packages/betterproto/__init__.py", line 747, in __post_init__
    for field_name, meta in self._betterproto.meta_by_field_name.items():
                            ^^^^^^^^^^^^^^^^^
  File "/home/bb/.cache/pypoetry/virtualenvs/testproto-ZoH_3K0Y-py3.12/lib/python3.12/site-packages/betterproto/__init__.py", line 838, in __getattribute__
    value = super().__getattribute__(name)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/bb/.cache/pypoetry/virtualenvs/testproto-ZoH_3K0Y-py3.12/lib/python3.12/site-packages/betterproto/utils.py", line 56, in __get__
    return self.__func__(type)
           ^^^^^^^^^^^^^^^^^^^
  File "/home/bb/.cache/pypoetry/virtualenvs/testproto-ZoH_3K0Y-py3.12/lib/python3.12/site-packages/betterproto/__init__.py", line 903, in _betterproto
    cls._betterproto_meta = meta = ProtoClassMetadata(cls)
                                   ^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/bb/.cache/pypoetry/virtualenvs/testproto-ZoH_3K0Y-py3.12/lib/python3.12/site-packages/betterproto/__init__.py", line 683, in __init__
    self.default_gen = self._get_default_gen(cls, fields)
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/bb/.cache/pypoetry/virtualenvs/testproto-ZoH_3K0Y-py3.12/lib/python3.12/site-packages/betterproto/__init__.py", line 690, in _get_default_gen
    return {field.name: cls._get_field_default_gen(field) for field in fields}
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/bb/.cache/pypoetry/virtualenvs/testproto-ZoH_3K0Y-py3.12/lib/python3.12/site-packages/betterproto/__init__.py", line 1183, in _get_field_default_gen
    elif issubclass(t, Enum):
         ^^^^^^^^^^^^^^^^^^^
TypeError: issubclass() arg 1 must be a class

```

### System Information

pydantic 2.8.2

libprotoc 27.2
Python 3.12.4
Name: betterproto
Version: 2.0.0b6
Summary: A better Protobuf / gRPC generator & library
Home-page: https://github.com/danielgtaylor/python-betterproto
Author: Daniel G. Taylor
Author-email: danielgtaylor@gmail.com
License: MIT
Location: /home/bb/.cache/pypoetry/virtualenvs/testproto-ZoH_3K0Y-py3.12/lib/python3.12/site-packages
Requires: grpclib, python-dateutil, typing-extensions
Required-by: testproto

### Checklist

- [X] I have searched the issues for duplicates.
- [X] I have shown the entire traceback, if possible.
- [X] I have verified this issue occurs on the latest prelease of betterproto which can be installed using `pip install -U --pre betterproto`, if possible.

Discussion:

Patch:
diff --git a/src/betterproto/__init__.py b/src/betterproto/__init__.py
index 050f414..cbafc83 100644
--- a/src/betterproto/__init__.py
+++ b/src/betterproto/__init__.py
@@ -62,6 +62,13 @@ if TYPE_CHECKING:
         SupportsWrite,
     )
 
+if sys.version_info >= (3, 10):
+    from types import UnionType as _types_UnionType
+else:
+
+    class _types_UnionType:
+        ...
+
 
 # Proto 3 data types
 TYPE_ENUM = "enum"
@@ -148,6 +155,7 @@ def datetime_default_gen() -> datetime:
 
 DATETIME_ZERO = datetime_default_gen()
 
+
 # Special protobuf json doubles
 INFINITY = "Infinity"
 NEG_INFINITY = "-Infinity"
@@ -1166,30 +1174,29 @@ class Message(ABC):
     def _get_field_default_gen(cls, field: dataclasses.Field) -> Any:
         t = cls._type_hint(field.name)
 
-        if hasattr(t, "__origin__"):
-            if t.__origin__ is dict:
-                # This is some kind of map (dict in Python).
-                return dict
-            elif t.__origin__ is list:
-                # This is some kind of list (repeated) field.
-                return list
-            elif t.__origin__ is Union and t.__args__[1] is type(None):
+        is_310_union = isinstance(t, _types_UnionType)
+        if hasattr(t, "__origin__") or is_310_union:
+            if is_310_union or t.__origin__ is Union:
                 # This is an optional field (either wrapped, or using proto3
                 # field presence). For setting the default we really don't care
                 # what kind of field it is.
                 return type(None)
-            else:
-                return t
-        elif issubclass(t, Enum):
+            if t.__origin__ is list:
+                # This is some kind of list (repeated) field.
+                return list
+            if t.__origin__ is dict:
+                # This is some kind of map (dict in Python).
+                return dict
+            return t
+        if issubclass(t, Enum):
             # Enums always default to zero.
             return t.try_value
-        elif t is datetime:
+        if t is datetime:
             # Offsets are relative to 1970-01-01T00:00:00Z
             return datetime_default_gen
-        else:
-            # This is either a primitive scalar or another message type. Calling
-            # it should result in its zero value.
-            return t
+        # This is either a primitive scalar or another message type. Calling
+        # it should result in its zero value.
+        return t
 
     def _postprocess_single(
         self, wire_type: int, meta: FieldMetadata, field_name: str, value: Any
diff --git a/src/betterproto/compile/importing.py b/src/betterproto/compile/importing.py
index 4221122..b216dfc 100644
--- a/src/betterproto/compile/importing.py
+++ b/src/betterproto/compile/importing.py
@@ -1,6 +1,9 @@
+from __future__ import annotations
+
 import os
 import re
 from typing import (
+    TYPE_CHECKING,
     Dict,
     List,
     Set,
@@ -13,6 +16,9 @@ from ..lib.google import protobuf as google_protobuf
 from .naming import pythonize_class_name
 
 
+if TYPE_CHECKING:
+    from ..plugin.typing_compiler import TypingCompiler
+
 WRAPPER_TYPES: Dict[str, Type] = {
     ".google.protobuf.DoubleValue": google_protobuf.DoubleValue,
     ".google.protobuf.FloatValue": google_protobuf.FloatValue,
@@ -47,7 +53,7 @@ def get_type_reference(
     package: str,
     imports: set,
     source_type: str,
-    typing_compiler: "TypingCompiler",
+    typing_compiler: TypingCompiler,
     unwrap: bool = True,
     pydantic: bool = False,
 ) -> str:
diff --git a/src/betterproto/plugin/typing_compiler.py b/src/betterproto/plugin/typing_compiler.py
index 937c7bf..eca3691 100644
--- a/src/betterproto/plugin/typing_compiler.py
+++ b/src/betterproto/plugin/typing_compiler.py
@@ -139,29 +139,35 @@ class TypingImportTypingCompiler(TypingCompiler):
 class NoTyping310TypingCompiler(TypingCompiler):
     _imports: Dict[str, Set[str]] = field(default_factory=lambda: defaultdict(set))
 
+    @staticmethod
+    def _fmt(type: str) -> str:  # for now this is necessary till 3.14
+        if type.startswith('"'):
+            return type[1:-1]
+        return type
+
     def optional(self, type: str) -> str:
-        return f"{type} | None"
+        return f'"{self._fmt(type)} | None"'
 
     def list(self, type: str) -> str:
-        return f"list[{type}]"
+        return f'"list[{self._fmt(type)}]"'
 
     def dict(self, key: str, value: str) -> str:
-        return f"dict[{key}, {value}]"
+        return f'"dict[{key}, {self._fmt(value)}]"'
 
     def union(self, *types: str) -> str:
-        return " | ".join(types)
+        return f'"{" | ".join(map(self._fmt, types))}"'
 
     def iterable(self, type: str) -> str:
-        self._imports["typing"].add("Iterable")
-        return f"Iterable[{type}]"
+        self._imports["collections.abc"].add("Iterable")
+        return f'"Iterable[{type}]"'
 
     def async_iterable(self, type: str) -> str:
-        self._imports["typing"].add("AsyncIterable")
-        return f"AsyncIterable[{type}]"
+        self._imports["collections.abc"].add("AsyncIterable")
+        return f'"AsyncIterable[{type}]"'
 
     def async_iterator(self, type: str) -> str:
-        self._imports["typing"].add("AsyncIterator")
-        return f"AsyncIterator[{type}]"
+        self._imports["collections.abc"].add("AsyncIterator")
+        return f'"AsyncIterator[{type}]"'
 
     def imports(self) -> Dict[str, Optional[Set[str]]]:
         return {k: v if v else None for k, v in self._imports.items()}


Test patch:
diff --git a/tests/test_typing_compiler.py b/tests/test_typing_compiler.py
index 3d1083c..ee17449 100644
--- a/tests/test_typing_compiler.py
+++ b/tests/test_typing_compiler.py
@@ -62,19 +62,17 @@ def test_typing_import_typing_compiler():
 def test_no_typing_311_typing_compiler():
     compiler = NoTyping310TypingCompiler()
     assert compiler.imports() == {}
-    assert compiler.optional("str") == "str | None"
+    assert compiler.optional("str") == '"str | None"'
     assert compiler.imports() == {}
-    assert compiler.list("str") == "list[str]"
+    assert compiler.list("str") == '"list[str]"'
     assert compiler.imports() == {}
-    assert compiler.dict("str", "int") == "dict[str, int]"
+    assert compiler.dict("str", "int") == '"dict[str, int]"'
     assert compiler.imports() == {}
-    assert compiler.union("str", "int") == "str | int"
+    assert compiler.union("str", "int") == '"str | int"'
     assert compiler.imports() == {}
-    assert compiler.iterable("str") == "Iterable[str]"
-    assert compiler.imports() == {"typing": {"Iterable"}}
-    assert compiler.async_iterable("str") == "AsyncIterable[str]"
-    assert compiler.imports() == {"typing": {"Iterable", "AsyncIterable"}}
-    assert compiler.async_iterator("str") == "AsyncIterator[str]"
+    assert compiler.iterable("str") == '"Iterable[str]"'
+    assert compiler.async_iterable("str") == '"AsyncIterable[str]"'
+    assert compiler.async_iterator("str") == '"AsyncIterator[str]"'
     assert compiler.imports() == {
-        "typing": {"Iterable", "AsyncIterable", "AsyncIterator"}
+        "collections.abc": {"Iterable", "AsyncIterable", "AsyncIterator"}
     }