2025-12-01

This commit is contained in:
2026-03-17 14:58:51 -06:00
parent 183e865f8b
commit 4b82b57113
6846 changed files with 954887 additions and 162606 deletions
@@ -0,0 +1,65 @@
"""Python Utils
This package contains dependency-free Python utility functions used throughout the
codebase.
Each utility should belong in its own file and be the default export.
These functions are not part of the module interface and are subject to change.
"""
from .convert_case import camel_to_snake, snake_to_camel
from .cached_property import cached_property
from .description import (
Description,
is_description,
register_description,
unregister_description,
)
from .did_you_mean import did_you_mean
from .group_by import group_by
from .identity_func import identity_func
from .inspect import inspect
from .is_awaitable import is_awaitable
from .is_iterable import is_collection, is_iterable
from .natural_compare import natural_comparison_key
from .awaitable_or_value import AwaitableOrValue
from .suggestion_list import suggestion_list
from .frozen_error import FrozenError
from .frozen_list import FrozenList
from .frozen_dict import FrozenDict
from .merge_kwargs import merge_kwargs
from .path import Path
from .print_path_list import print_path_list
from .simple_pub_sub import SimplePubSub, SimplePubSubIterator
from .undefined import Undefined, UndefinedType
__all__ = [
"camel_to_snake",
"snake_to_camel",
"cached_property",
"did_you_mean",
"Description",
"group_by",
"is_description",
"register_description",
"unregister_description",
"identity_func",
"inspect",
"is_awaitable",
"is_collection",
"is_iterable",
"merge_kwargs",
"natural_comparison_key",
"AwaitableOrValue",
"suggestion_list",
"FrozenError",
"FrozenList",
"FrozenDict",
"Path",
"print_path_list",
"SimplePubSub",
"SimplePubSubIterator",
"Undefined",
"UndefinedType",
]
@@ -0,0 +1,8 @@
from typing import Awaitable, TypeVar, Union
__all__ = ["AwaitableOrValue"]
T = TypeVar("T")
AwaitableOrValue = Union[Awaitable[T], T]
@@ -0,0 +1,35 @@
from typing import Any, Callable, TYPE_CHECKING
if TYPE_CHECKING:
standard_cached_property = None
else:
try:
from functools import cached_property as standard_cached_property
except ImportError: # Python < 3.8
standard_cached_property = None
if standard_cached_property:
cached_property = standard_cached_property
else:
# Code taken from https://github.com/bottlepy/bottle
class CachedProperty:
"""A cached property.
A property that is only computed once per instance and then replaces itself with
an ordinary attribute. Deleting the attribute resets the property.
"""
def __init__(self, func: Callable) -> None:
self.__doc__ = getattr(func, "__doc__")
self.func = func
def __get__(self, obj: object, cls: type) -> Any:
if obj is None:
return self
value = obj.__dict__[self.func.__name__] = self.func(obj)
return value
cached_property = CachedProperty
__all__ = ["cached_property"]
@@ -0,0 +1,25 @@
# uses code from https://github.com/daveoncode/python-string-utils
import re
__all__ = ["camel_to_snake", "snake_to_camel"]
_re_camel_to_snake = re.compile(r"([a-z]|[A-Z0-9]+)(?=[A-Z])")
_re_snake_to_camel = re.compile(r"(_)([a-z\d])")
def camel_to_snake(s: str) -> str:
"""Convert from CamelCase to snake_case"""
return _re_camel_to_snake.sub(r"\1_", s).lower()
def snake_to_camel(s: str, upper: bool = True) -> str:
"""Convert from snake_case to CamelCase
If upper is set, then convert to upper CamelCase, otherwise the first character
keeps its case.
"""
s = _re_snake_to_camel.sub(lambda m: m.group(2).upper(), s)
if upper:
s = s[:1].upper() + s[1:]
return s
@@ -0,0 +1,59 @@
from typing import Any, Tuple, Union
__all__ = [
"Description",
"is_description",
"register_description",
"unregister_description",
]
class Description:
"""Type checker for human readable descriptions.
By default, only ordinary strings are accepted as descriptions,
but you can register() other classes that will also be allowed,
e.g. to support lazy string objects that are evaluated only at runtime.
If you register(object), any object will be allowed as description.
"""
bases: Union[type, Tuple[type, ...]] = str
@classmethod
def isinstance(cls, obj: Any) -> bool:
return isinstance(obj, cls.bases)
@classmethod
def register(cls, base: type) -> None:
"""Register a class that shall be accepted as a description."""
if not isinstance(base, type):
raise TypeError("Only types can be registered.")
if base is object:
cls.bases = object
elif cls.bases is object:
cls.bases = base
elif not isinstance(cls.bases, tuple):
if base is not cls.bases:
cls.bases = (cls.bases, base)
elif base not in cls.bases:
cls.bases += (base,)
@classmethod
def unregister(cls, base: type) -> None:
"""Unregister a class that shall no more be accepted as a description."""
if not isinstance(base, type):
raise TypeError("Only types can be unregistered.")
if isinstance(cls.bases, tuple):
if base in cls.bases: # pragma: no branch
cls.bases = tuple(b for b in cls.bases if b is not base)
if not cls.bases:
cls.bases = object
elif len(cls.bases) == 1:
cls.bases = cls.bases[0]
elif cls.bases is base:
cls.bases = object
is_description = Description.isinstance
register_description = Description.register
unregister_description = Description.unregister
@@ -0,0 +1,28 @@
from typing import Optional, Sequence
__all__ = ["did_you_mean"]
MAX_LENGTH = 5
def did_you_mean(suggestions: Sequence[str], sub_message: Optional[str] = None) -> str:
"""Given [ A, B, C ] return ' Did you mean A, B, or C?'"""
if not suggestions or not MAX_LENGTH:
return ""
parts = [" Did you mean "]
if sub_message:
parts.extend([sub_message, " "])
suggestions = suggestions[:MAX_LENGTH]
n = len(suggestions)
if n == 1:
parts.append(f"'{suggestions[0]}'?")
elif n == 2:
parts.append(f"'{suggestions[0]}' or '{suggestions[1]}'?")
else:
parts.extend(
[
", ".join(f"'{s}'" for s in suggestions[:-1]),
f", or '{suggestions[-1]}'?",
]
)
return "".join(parts)
@@ -0,0 +1,52 @@
from copy import deepcopy
from typing import Dict, TypeVar
from .frozen_error import FrozenError
__all__ = ["FrozenDict"]
KT = TypeVar("KT")
VT = TypeVar("VT")
class FrozenDict(Dict[KT, VT]):
"""Dictionary that can only be read, but not changed.
.. deprecated:: 3.2
Use dicts and the Mapping type instead. Will be removed in v3.3.
"""
def __delitem__(self, key):
raise FrozenError
def __setitem__(self, key, value):
raise FrozenError
def __iadd__(self, value):
raise FrozenError
def __hash__(self) -> int: # type: ignore
return hash(tuple(self.items()))
def __copy__(self) -> "FrozenDict":
return FrozenDict(self)
copy = __copy__
def __deepcopy__(self, memo: Dict) -> "FrozenDict":
return FrozenDict({k: deepcopy(v, memo) for k, v in self.items()})
def clear(self):
raise FrozenError
def pop(self, key, default=None):
raise FrozenError
def popitem(self):
raise FrozenError
def setdefault(self, key, default=None):
raise FrozenError
def update(self, other=None): # type: ignore
raise FrozenError
@@ -0,0 +1,5 @@
__all__ = ["FrozenError"]
class FrozenError(TypeError):
"""Error when trying to change a frozen (read only) collection."""
@@ -0,0 +1,70 @@
from copy import deepcopy
from typing import Dict, List, TypeVar
from .frozen_error import FrozenError
__all__ = ["FrozenList"]
T = TypeVar("T")
class FrozenList(List[T]):
"""List that can only be read, but not changed.
.. deprecated:: 3.2
Use tuples or lists and the Collection type instead. Will be removed in v3.3.
"""
def __delitem__(self, key):
raise FrozenError
def __setitem__(self, key, value):
raise FrozenError
def __add__(self, value):
if isinstance(value, tuple):
value = list(value)
return list.__add__(self, value)
def __iadd__(self, value):
raise FrozenError
def __mul__(self, value):
return list.__mul__(self, value)
def __imul__(self, value):
raise FrozenError
def __hash__(self) -> int: # type: ignore
return hash(tuple(self))
def __copy__(self) -> "FrozenList":
return FrozenList(self)
def __deepcopy__(self, memo: Dict) -> "FrozenList":
return FrozenList(deepcopy(value, memo) for value in self)
def append(self, x):
raise FrozenError
def extend(self, iterable):
raise FrozenError
def insert(self, i, x):
raise FrozenError
def remove(self, x):
raise FrozenError
def pop(self, i=None):
raise FrozenError
def clear(self):
raise FrozenError
def sort(self, *, key=None, reverse=False):
raise FrozenError
def reverse(self):
raise FrozenError
@@ -0,0 +1,16 @@
from collections import defaultdict
from typing import Callable, Collection, Dict, List, TypeVar
__all__ = ["group_by"]
K = TypeVar("K")
T = TypeVar("T")
def group_by(items: Collection[T], key_fn: Callable[[T], K]) -> Dict[K, List[T]]:
"""Group an unsorted collection of items by a key derived via a function."""
result: Dict[K, List[T]] = defaultdict(list)
for item in items:
key = key_fn(item)
result[key].append(item)
return result
@@ -0,0 +1,13 @@
from typing import cast, Any, TypeVar
from .undefined import Undefined
__all__ = ["identity_func"]
T = TypeVar("T")
def identity_func(x: T = cast(Any, Undefined), *_args: Any) -> T:
"""Return the first received argument."""
return x
@@ -0,0 +1,182 @@
from inspect import (
isclass,
ismethod,
isfunction,
isgeneratorfunction,
isgenerator,
iscoroutinefunction,
iscoroutine,
isasyncgenfunction,
isasyncgen,
)
from typing import Any, List
from .undefined import Undefined
__all__ = ["inspect"]
max_recursive_depth = 2
max_str_size = 240
max_list_size = 10
def inspect(value: Any) -> str:
"""Inspect value and a return string representation for error messages.
Used to print values in error messages. We do not use repr() in order to not
leak too much of the inner Python representation of unknown objects, and we
do not use json.dumps() because not all objects can be serialized as JSON and
we want to output strings with single quotes like Python repr() does it.
We also restrict the size of the representation by truncating strings and
collections and allowing only a maximum recursion depth.
"""
return inspect_recursive(value, [])
def inspect_recursive(value: Any, seen_values: List) -> str:
if value is None or value is Undefined or isinstance(value, (bool, float, complex)):
return repr(value)
if isinstance(value, (int, str, bytes, bytearray)):
return trunc_str(repr(value))
if len(seen_values) < max_recursive_depth and value not in seen_values:
# check if we have a custom inspect method
inspect_method = getattr(value, "__inspect__", None)
if inspect_method is not None and callable(inspect_method):
s = inspect_method()
if isinstance(s, str):
return trunc_str(s)
seen_values = [*seen_values, value]
return inspect_recursive(s, seen_values)
# recursively inspect collections
if isinstance(value, (list, tuple, dict, set, frozenset)):
if not value:
return repr(value)
seen_values = [*seen_values, value]
if isinstance(value, list):
items = value
elif isinstance(value, dict):
items = list(value.items())
else:
items = list(value)
items = trunc_list(items)
if isinstance(value, dict):
s = ", ".join(
(
"..."
if v is ELLIPSIS
else inspect_recursive(v[0], seen_values)
+ ": "
+ inspect_recursive(v[1], seen_values)
)
for v in items
)
else:
s = ", ".join(
"..." if v is ELLIPSIS else inspect_recursive(v, seen_values)
for v in items
)
if isinstance(value, tuple):
if len(items) == 1:
return f"({s},)"
return f"({s})"
if isinstance(value, (dict, set)):
return "{" + s + "}"
if isinstance(value, frozenset):
return f"frozenset({{{s}}})"
return f"[{s}]"
else:
# handle collections that are nested too deep
if isinstance(value, (list, tuple, dict, set, frozenset)):
if not value:
return repr(value)
if isinstance(value, list):
return "[...]"
if isinstance(value, tuple):
return "(...)"
if isinstance(value, dict):
return "{...}"
if isinstance(value, set):
return "set(...)"
return "frozenset(...)"
if isinstance(value, Exception):
type_ = "exception"
value = type(value)
elif isclass(value):
type_ = "exception class" if issubclass(value, Exception) else "class"
elif ismethod(value):
type_ = "method"
elif iscoroutinefunction(value):
type_ = "coroutine function"
elif isasyncgenfunction(value):
type_ = "async generator function"
elif isgeneratorfunction(value):
type_ = "generator function"
elif isfunction(value):
type_ = "function"
elif iscoroutine(value):
type_ = "coroutine"
elif isasyncgen(value):
type_ = "async generator"
elif isgenerator(value):
type_ = "generator"
else:
# stringify (only) the well-known GraphQL types
from ..type import (
GraphQLDirective,
GraphQLNamedType,
GraphQLScalarType,
GraphQLWrappingType,
)
if isinstance(
value,
(
GraphQLDirective,
GraphQLNamedType,
GraphQLScalarType,
GraphQLWrappingType,
),
):
return str(value)
try:
name = type(value).__name__
if not name or "<" in name or ">" in name:
raise AttributeError
except AttributeError:
return "<object>"
else:
return f"<{name} instance>"
try:
name = value.__name__
if not name or "<" in name or ">" in name:
raise AttributeError
except AttributeError:
return f"<{type_}>"
else:
return f"<{type_} {name}>"
def trunc_str(s: str) -> str:
"""Truncate strings to maximum length."""
if len(s) > max_str_size:
i = max(0, (max_str_size - 3) // 2)
j = max(0, max_str_size - 3 - i)
s = s[:i] + "..." + s[-j:]
return s
def trunc_list(s: List) -> List:
"""Truncate lists to maximum length."""
if len(s) > max_list_size:
i = max_list_size // 2
j = i - 1
s = s[:i] + [ELLIPSIS] + s[-j:]
return s
class InspectEllipsisType:
"""Singleton class for indicating ellipses in iterables."""
ELLIPSIS = InspectEllipsisType()
@@ -0,0 +1,24 @@
import inspect
from typing import Any
from types import CoroutineType, GeneratorType
__all__ = ["is_awaitable"]
CO_ITERABLE_COROUTINE = inspect.CO_ITERABLE_COROUTINE
def is_awaitable(value: Any) -> bool:
"""Return true if object can be passed to an ``await`` expression.
Instead of testing if the object is an instance of abc.Awaitable, it checks
the existence of an `__await__` attribute. This is much faster.
"""
return (
# check for coroutine objects
isinstance(value, CoroutineType)
# check for old-style generator based coroutine objects
or isinstance(value, GeneratorType)
and bool(value.gi_code.co_flags & CO_ITERABLE_COROUTINE)
# check for other awaitables (e.g. futures)
or hasattr(value, "__await__")
)
@@ -0,0 +1,24 @@
from collections.abc import Collection, Iterable, Mapping, ValuesView
from typing import Any
__all__ = ["is_collection", "is_iterable"]
collection_types: Any = Collection
if not isinstance({}.values(), Collection): # Python < 3.7.2
collection_types = (Collection, ValuesView)
iterable_types: Any = Iterable
not_iterable_types: Any = (bytes, bytearray, memoryview, str, Mapping)
def is_collection(value: Any) -> bool:
"""Check if value is a collection, but not a string or a mapping."""
return isinstance(value, collection_types) and not isinstance(
value, not_iterable_types
)
def is_iterable(value: Any) -> bool:
"""Check if value is an iterable, but not a string or a mapping."""
return isinstance(value, iterable_types) and not isinstance(
value, not_iterable_types
)
@@ -0,0 +1,8 @@
from typing import cast, Any, Dict, TypeVar
T = TypeVar("T")
def merge_kwargs(base_dict: T, **kwargs: Any) -> T:
"""Return arbitrary typed dictionary with some keyword args merged in."""
return cast(T, {**cast(Dict, base_dict), **kwargs})
@@ -0,0 +1,19 @@
import re
from typing import Tuple
from itertools import cycle
__all__ = ["natural_comparison_key"]
_re_digits = re.compile(r"(\d+)")
def natural_comparison_key(key: str) -> Tuple:
"""Comparison key function for sorting strings by natural sort order.
See: https://en.wikipedia.org/wiki/Natural_sort_order
"""
return tuple(
(int(part), part) if is_digit else part
for part, is_digit in zip(_re_digits.split(key), cycle((False, True)))
)
@@ -0,0 +1,28 @@
from typing import Any, List, NamedTuple, Optional, Union
__all__ = ["Path"]
class Path(NamedTuple):
"""A generic path of string or integer indices"""
prev: Any # Optional['Path'] (python/mypy/issues/731)
"""path with the previous indices"""
key: Union[str, int]
"""current index in the path (string or integer)"""
typename: Optional[str]
"""name of the parent type to avoid path ambiguity"""
def add_key(self, key: Union[str, int], typename: Optional[str] = None) -> "Path":
"""Return a new Path containing the given key."""
return Path(self, key, typename)
def as_list(self) -> List[Union[str, int]]:
"""Return a list of the path keys."""
flattened: List[Union[str, int]] = []
append = flattened.append
curr: Path = self
while curr:
append(curr.key)
curr = curr.prev
return flattened[::-1]
@@ -0,0 +1,6 @@
from typing import Collection, Union
def print_path_list(path: Collection[Union[str, int]]) -> str:
"""Build a string describing the path."""
return "".join(f"[{key}]" if isinstance(key, int) else f".{key}" for key in path)
@@ -0,0 +1,81 @@
from asyncio import Future, Queue, ensure_future, sleep
from inspect import isawaitable
from typing import Any, AsyncIterator, Callable, Optional, Set
try:
from asyncio import get_running_loop
except ImportError:
from asyncio import get_event_loop as get_running_loop # Python < 3.7
__all__ = ["SimplePubSub", "SimplePubSubIterator"]
class SimplePubSub:
"""A very simple publish-subscript system.
Creates an AsyncIterator from an EventEmitter.
Useful for mocking a PubSub system for tests.
"""
subscribers: Set[Callable]
def __init__(self) -> None:
self.subscribers = set()
def emit(self, event: Any) -> bool:
"""Emit an event."""
for subscriber in self.subscribers:
result = subscriber(event)
if isawaitable(result):
ensure_future(result)
return bool(self.subscribers)
def get_subscriber(
self, transform: Optional[Callable] = None
) -> "SimplePubSubIterator":
return SimplePubSubIterator(self, transform)
class SimplePubSubIterator(AsyncIterator):
def __init__(self, pubsub: SimplePubSub, transform: Optional[Callable]) -> None:
self.pubsub = pubsub
self.transform = transform
self.pull_queue: Queue[Future] = Queue()
self.push_queue: Queue[Any] = Queue()
self.listening = True
pubsub.subscribers.add(self.push_value)
def __aiter__(self) -> "SimplePubSubIterator":
return self
async def __anext__(self) -> Any:
if not self.listening:
raise StopAsyncIteration
await sleep(0)
if not self.push_queue.empty():
return await self.push_queue.get()
future = get_running_loop().create_future()
await self.pull_queue.put(future)
return future
async def aclose(self) -> None:
if self.listening:
await self.empty_queue()
async def empty_queue(self) -> None:
self.listening = False
self.pubsub.subscribers.remove(self.push_value)
while not self.pull_queue.empty():
future = await self.pull_queue.get()
future.cancel()
while not self.push_queue.empty():
await self.push_queue.get()
async def push_value(self, event: Any) -> None:
value = event if self.transform is None else self.transform(event)
if self.pull_queue.empty():
await self.push_queue.put(value)
else:
(await self.pull_queue.get()).set_result(value)
@@ -0,0 +1,109 @@
from typing import Collection, Optional, List
from .natural_compare import natural_comparison_key
__all__ = ["suggestion_list"]
def suggestion_list(input_: str, options: Collection[str]) -> List[str]:
"""Get list with suggestions for a given input.
Given an invalid input string and list of valid options, returns a filtered list
of valid options sorted based on their similarity with the input.
"""
options_by_distance = {}
lexical_distance = LexicalDistance(input_)
threshold = int(len(input_) * 0.4) + 1
for option in options:
distance = lexical_distance.measure(option, threshold)
if distance is not None:
options_by_distance[option] = distance
# noinspection PyShadowingNames
return sorted(
options_by_distance,
key=lambda option: (
options_by_distance.get(option, 0),
natural_comparison_key(option),
),
)
class LexicalDistance:
"""Computes the lexical distance between strings A and B.
The "distance" between two strings is given by counting the minimum number of edits
needed to transform string A into string B. An edit can be an insertion, deletion,
or substitution of a single character, or a swap of two adjacent characters.
This distance can be useful for detecting typos in input or sorting.
"""
_input: str
_input_lower_case: str
_input_list: List[int]
_rows: List[List[int]]
def __init__(self, input_: str):
self._input = input_
self._input_lower_case = input_.lower()
row_size = len(input_) + 1
self._input_list = list(map(ord, self._input_lower_case))
self._rows = [[0] * row_size, [0] * row_size, [0] * row_size]
def measure(self, option: str, threshold: int) -> Optional[int]:
if self._input == option:
return 0
option_lower_case = option.lower()
# Any case change counts as a single edit
if self._input_lower_case == option_lower_case:
return 1
a, b = list(map(ord, option_lower_case)), self._input_list
a_len, b_len = len(a), len(b)
if a_len < b_len:
a, b = b, a
a_len, b_len = b_len, a_len
if a_len - b_len > threshold:
return None
rows = self._rows
for j in range(b_len + 1):
rows[0][j] = j
for i in range(1, a_len + 1):
up_row = rows[(i - 1) % 3]
current_row = rows[i % 3]
smallest_cell = current_row[0] = i
for j in range(1, b_len + 1):
cost = 0 if a[i - 1] == b[j - 1] else 1
current_cell = min(
up_row[j] + 1, # delete
current_row[j - 1] + 1, # insert
up_row[j - 1] + cost, # substitute
)
if i > 1 and j > 1 and a[i - 1] == b[j - 2] and a[i - 2] == b[j - 1]:
# transposition
double_diagonal_cell = rows[(i - 2) % 3][j - 2]
current_cell = min(current_cell, double_diagonal_cell + 1)
if current_cell < smallest_cell:
smallest_cell = current_cell
current_row[j] = current_cell
# Early exit, since distance can't go smaller than smallest element
# of the previous row.
if smallest_cell > threshold:
return None
distance = rows[a_len % 3][b_len]
return distance if distance <= threshold else None
@@ -0,0 +1,34 @@
from typing import Any
__all__ = ["Undefined", "UndefinedType"]
class UndefinedType(ValueError):
"""Auxiliary class for creating the Undefined singleton."""
def __repr__(self) -> str:
return "Undefined"
__str__ = __repr__
def __hash__(self) -> int:
return hash(UndefinedType)
def __bool__(self) -> bool:
return False
def __eq__(self, other: Any) -> bool:
return other is Undefined
def __ne__(self, other: Any) -> bool:
return not self == other
# Used to indicate undefined or invalid values (like "undefined" in JavaScript):
Undefined = UndefinedType()
Undefined.__doc__ = """Symbol for undefined values
This singleton object is used to describe undefined or invalid values.
It can be used in places where you would use ``undefined`` in GraphQL.js.
"""