mirror of
https://github.com/element-hq/synapse.git
synced 2026-04-26 15:17:44 +00:00
Add explicit Absent utility type
This commit is contained in:
@@ -29,6 +29,7 @@ from typing import (
|
||||
AbstractSet,
|
||||
Any,
|
||||
ClassVar,
|
||||
Final,
|
||||
Literal,
|
||||
Mapping,
|
||||
Match,
|
||||
@@ -42,7 +43,10 @@ from typing import (
|
||||
)
|
||||
|
||||
import attr
|
||||
import pydantic_core.core_schema
|
||||
from immutabledict import immutabledict
|
||||
from pydantic import GetCoreSchemaHandler
|
||||
from pydantic_core import CoreSchema
|
||||
from signedjson.key import decode_verify_key_bytes
|
||||
from signedjson.types import VerifyKey
|
||||
from typing_extensions import Self
|
||||
@@ -109,6 +113,67 @@ StrCollection = tuple[str, ...] | list[str] | AbstractSet[str]
|
||||
StrSequence = tuple[str, ...] | list[str]
|
||||
|
||||
|
||||
class AbsentType(Enum):
|
||||
"""
|
||||
Type of a sentinel to use as an alternative to `None`
|
||||
for when we really mean 'absent' and not JSON null.
|
||||
|
||||
For a Sentinel for internal (non-API-facing) use, instead consider
|
||||
`Sentinel.UNSET_SENTINEL`.
|
||||
|
||||
It is falsy (like None is), so shorthand forms like `x or 0` can be used.
|
||||
"""
|
||||
|
||||
# Making this an Enum member makes this compatible with type narrowing,
|
||||
# meaning `x is not Absent` will narrow `x: int | AbsentType` to `x: int` etc.
|
||||
_Absent = object()
|
||||
|
||||
@classmethod
|
||||
def __get_pydantic_core_schema__(
|
||||
cls, source_type: object, handler: GetCoreSchemaHandler
|
||||
) -> CoreSchema:
|
||||
return pydantic_core.core_schema.is_instance_schema(cls)
|
||||
|
||||
def __copy__(self) -> "AbsentType":
|
||||
"""
|
||||
Copy implementation used by `copy.copy()`.
|
||||
Always use the same instance.
|
||||
|
||||
Without this and the deep version `__deepcopy__`,
|
||||
`copy.copy(Absent)` on Python 3.10 (olddeps)
|
||||
had a problem where it tried to construct a new Absent
|
||||
as part of a deepcopy operation and resulted in:
|
||||
ValueError: <object object at 0x7f64b3b6d930> is not a valid AbsentType
|
||||
"""
|
||||
return self
|
||||
|
||||
def __deepcopy__(self, memo: object) -> "AbsentType":
|
||||
"""
|
||||
Copy implementation used by `copy.deepcopy()`.
|
||||
Always use the same instance.
|
||||
"""
|
||||
return self
|
||||
|
||||
def __bool__(self) -> Literal[False]:
|
||||
return False
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "Absent"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "Absent"
|
||||
|
||||
|
||||
Absent: Final = AbsentType._Absent
|
||||
"""
|
||||
Sentinel to use as an alternative to `None`
|
||||
for when we really mean 'absent' and not JSON null.
|
||||
|
||||
For a Sentinel for internal (non-API-facing) use, instead consider
|
||||
`Sentinel.UNSET_SENTINEL`.
|
||||
"""
|
||||
|
||||
|
||||
# Note that this seems to require inheriting *directly* from Interface in order
|
||||
# for mypy-zope to realize it is an interface.
|
||||
class ISynapseThreadlessReactor(
|
||||
|
||||
@@ -16,6 +16,17 @@ import enum
|
||||
|
||||
|
||||
class Sentinel(enum.Enum):
|
||||
"""
|
||||
Internal marker sentinel for distinguishing a default state from user-suppliable values.
|
||||
Has no meaning on its own.
|
||||
|
||||
Use this when you want to be absolutely sure that the marker came from Synapse code
|
||||
and not from request body parsing.
|
||||
|
||||
If you want a Pydantic-compatible Sentinel that is suitable for expressing
|
||||
'absent from some parsed JSON payload' or equivalent, see `Absent`.
|
||||
"""
|
||||
|
||||
# defining a sentinel in this way allows mypy to correctly handle the
|
||||
# type of a dictionary lookup and subsequent type narrowing.
|
||||
UNSET_SENTINEL = object()
|
||||
|
||||
@@ -18,14 +18,17 @@
|
||||
# [This file includes modifications made by New Vector Limited]
|
||||
#
|
||||
#
|
||||
|
||||
import copy
|
||||
from unittest import skipUnless
|
||||
|
||||
from immutabledict import immutabledict
|
||||
from parameterized import parameterized_class
|
||||
from pydantic import BaseModel, PydanticInvalidForJsonSchema, ValidationError
|
||||
|
||||
from synapse.api.errors import SynapseError
|
||||
from synapse.types import (
|
||||
Absent,
|
||||
AbsentType,
|
||||
AbstractMultiWriterStreamToken,
|
||||
MultiWriterStreamToken,
|
||||
RoomAlias,
|
||||
@@ -199,3 +202,79 @@ class MultiWriterTokenTestCase(unittest.HomeserverTestCase):
|
||||
|
||||
parsed_token = self.get_success(self.token_type.parse(store, "m5~"))
|
||||
self.assertEqual(parsed_token, self.token_type(stream=5))
|
||||
|
||||
|
||||
class AbsentTestCase(unittest.TestCase):
|
||||
"""
|
||||
Tests for the `Absent` utility, which is meant to be like `None` except
|
||||
explicitly signalling absence rather than JSON null.
|
||||
"""
|
||||
|
||||
def test_cant_create_second_absent(self) -> None:
|
||||
"""
|
||||
Tests that we aren't allowed to instantiate a second `Absent`.
|
||||
"""
|
||||
with self.assertRaises(TypeError):
|
||||
AbsentType() # type: ignore[call-arg]
|
||||
|
||||
def test_is_falsy(self) -> None:
|
||||
"""
|
||||
Tests `Absent` is falsy and can therefore be used a bit like `None`.
|
||||
"""
|
||||
if Absent:
|
||||
self.fail("Absent is truthy!")
|
||||
|
||||
self.assertEqual(Absent or "something", "something")
|
||||
|
||||
def test_pydantic_jsonschema(self) -> None:
|
||||
"""
|
||||
Tests that `Absent` can't be used to produce JSONSchema in Pydantic models.
|
||||
|
||||
In the future, it may be useful to produce correct JSONSchema, but for now
|
||||
I was mostly interested in making sure we don't produce weird/invalid JSONSchema.
|
||||
"""
|
||||
|
||||
class MyModel(BaseModel):
|
||||
absent: AbsentType = Absent
|
||||
|
||||
with self.assertRaises(PydanticInvalidForJsonSchema):
|
||||
MyModel.model_json_schema()
|
||||
|
||||
def test_pydantic_reject_null(self) -> None:
|
||||
"""
|
||||
Tests that `Absent` rejects `None` (JSON null) when used in Pydantic models.
|
||||
"""
|
||||
|
||||
class MyModel(BaseModel):
|
||||
absent: AbsentType = Absent
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
MyModel.model_validate({"absent": None})
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
MyModel.model_validate_json('{"absent": null}')
|
||||
|
||||
def test_pydantic_accept_absence(self) -> None:
|
||||
"""
|
||||
Tests that `Absent` accepts the absence of a value when used in Pydantic models.
|
||||
"""
|
||||
|
||||
class MyModel(BaseModel):
|
||||
absent: AbsentType = Absent
|
||||
|
||||
self.assertEqual(MyModel.model_validate({}), MyModel(absent=Absent))
|
||||
self.assertEqual(MyModel.model_validate_json("{}"), MyModel(absent=Absent))
|
||||
|
||||
def test_copy(self) -> None:
|
||||
"""
|
||||
Tests that the `copy` module always uses the same instance of Absent.
|
||||
"""
|
||||
|
||||
class MyModel(BaseModel):
|
||||
absent: AbsentType = Absent
|
||||
|
||||
a = MyModel.model_validate({})
|
||||
b = copy.deepcopy(a)
|
||||
|
||||
self.assertIs(copy.copy(Absent), Absent)
|
||||
self.assertIs(a.absent, b.absent)
|
||||
|
||||
Reference in New Issue
Block a user