Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: pydantic v1/v2 compat #84

Merged
merged 6 commits into from
Jan 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ jobs:
name: Test Coverage
runs-on: ${{ matrix.os }}
concurrency:
group: test-coverage-${{ github.ref }}-${{ matrix.os }}-${{ matrix.python-version }}
group: test-coverage-${{ github.ref }}-${{ matrix.os }}-${{ matrix.python-version }}-${{ matrix.env }}
cancel-in-progress: true
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
os: [ubuntu-latest, windows-latest, macos-latest]
env: [pydantic-v2]
env: [pydantic-v1, pydantic-v2]
fail-fast: false
env:
OS: ${{ matrix.os }}
Expand Down
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ poetry add githubkit[auth-oauth-device]
pdm add githubkit[auth-oauth-device]
```

githubkit supports **both pydantic v1 and v2**, but pydantic v2 is recommended. If you have encountered any problems with pydantic v1/v2, please file an issue.

## Usage

### Authentication
Expand Down Expand Up @@ -357,14 +359,21 @@ from githubkit.webhooks import parse
event = parse(request.headers["X-GitHub-Event"], request.body)
```

(NOT RECOMMENDED) Parse the payload without event name (may cost longer time and more memory):
(**NOT RECOMMENDED**) Parse the payload without event name (may cost longer time and more memory):

```python
from githubkit.webhooks import parse_without_name

event = parse_without_name(request.body)
```

> [!WARNING]
> The `parse_without_name` function will try to parse the payload with all supported event names.
> The behavior of this function is not the same between pydantic v1 and v2.
> When using pydantic v1, the function will return the first valid event model (known as `left-to-right` mode).
> When using pydantic v2, the function will return the highest scored valid event model (known as `smart` mode).
> See: [Union Modes](https://docs.pydantic.dev/latest/concepts/unions/#union-modes).

Parse dict like payload:

```python
Expand Down Expand Up @@ -429,7 +438,7 @@ Run tests in dev env:
./scripts/run-tests.sh
```

Run tests in test env:
Run tests in test envs, for example:

```bash
cd ./envs/pydantic-v2/
Expand Down
37 changes: 21 additions & 16 deletions codegen/parser/schemas/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ class SchemaData:

_type_string: ClassVar[str] = "Any"

def get_type_string(self) -> str:
def get_type_string(self, include_constraints: bool = True) -> str:
"""Get schema typing string in any place"""
if args := self._get_field_args():
if include_constraints and (args := self._get_field_args()):
return f"Annotated[{self._type_string}, {self._get_field_string(args)}]"
return self._type_string

Expand Down Expand Up @@ -66,9 +66,11 @@ class Property:
required: bool
schema_data: SchemaData

def get_type_string(self) -> str:
def get_type_string(self, include_constraints: bool = True) -> str:
"""Get schema typing string in any place"""
type_string = self.schema_data.get_type_string()
type_string = self.schema_data.get_type_string(
include_constraints=include_constraints
)
return type_string if self.required else f"Missing[{type_string}]"

def get_param_type_string(self) -> str:
Expand All @@ -78,8 +80,11 @@ def get_param_type_string(self) -> str:

def get_model_defination(self) -> str:
"""Get defination used by model codegen"""
type_ = self.get_type_string()
default = self._get_field_string(self._get_field_args())
# extract the outermost type constraints to the field
type_ = self.get_type_string(include_constraints=False)
args = self.schema_data._get_field_args()
args.update(self._get_field_args())
default = self._get_field_string(args)
return f"{self.prop_name}: {type_} = {default}"

def get_type_defination(self) -> str:
Expand Down Expand Up @@ -351,9 +356,9 @@ class ListSchema(SchemaData):
_type_string: ClassVar[str] = "List"

@override
def get_type_string(self) -> str:
def get_type_string(self, include_constraints: bool = True) -> str:
type_string = f"List[{self.item_schema.get_type_string()}]"
if args := self._get_field_args():
if include_constraints and (args := self._get_field_args()):
return f"Annotated[{type_string}, {self._get_field_string(args)}]"
return type_string

Expand Down Expand Up @@ -419,9 +424,9 @@ class UniqueListSchema(SchemaData):
_type_string: ClassVar[str] = "UniqueList"

@override
def get_type_string(self) -> str:
def get_type_string(self, include_constraints: bool = True) -> str:
type_string = f"UniqueList[{self.item_schema.get_type_string()}]"
if args := self._get_field_args():
if include_constraints and (args := self._get_field_args()):
return f"Annotated[{type_string}, {self._get_field_string(args)}]"
return type_string

Expand Down Expand Up @@ -495,9 +500,9 @@ def is_float_enum(self) -> bool:
return all(isinstance(value, int | float) for value in self.values)

@override
def get_type_string(self) -> str:
def get_type_string(self, include_constraints: bool = True) -> str:
type_string = f"Literal[{', '.join(repr(value) for value in self.values)}]"
if args := self._get_field_args():
if include_constraints and (args := self._get_field_args()):
return f"Annotated[{type_string}, {self._get_field_string(args)}]"
return type_string

Expand Down Expand Up @@ -539,8 +544,8 @@ class ModelSchema(SchemaData):
_type_string: ClassVar[str] = "dict"

@override
def get_type_string(self) -> str:
if args := self._get_field_args():
def get_type_string(self, include_constraints: bool = True) -> str:
if include_constraints and (args := self._get_field_args()):
return f"Annotated[{self.class_name}, {self._get_field_string(args)}]"
return self.class_name

Expand Down Expand Up @@ -595,15 +600,15 @@ class UnionSchema(SchemaData):
discriminator: str | None = None

@override
def get_type_string(self) -> str:
def get_type_string(self, include_constraints: bool = True) -> str:
if len(self.schemas) == 0:
return "Any"
elif len(self.schemas) == 1:
return self.schemas[0].get_type_string()
type_string = (
f"Union[{', '.join(schema.get_type_string() for schema in self.schemas)}]"
)
if args := self._get_field_args():
if include_constraints and (args := self._get_field_args()):
return f"Annotated[{type_string}, {self._get_field_string(args)}]"
return type_string

Expand Down
Loading