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

Upgrade typeguard, and add test that would have caught typeguard-related regression #2241

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
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
443 changes: 306 additions & 137 deletions metaflow/_vendor/typeguard/_checkers.py

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions metaflow/_vendor/typeguard/_config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

from collections.abc import Collection
from collections.abc import Iterable
from dataclasses import dataclass
from enum import Enum, auto
from typing import TYPE_CHECKING, TypeVar
Expand Down Expand Up @@ -49,11 +49,11 @@ class CollectionCheckStrategy(Enum):
FIRST_ITEM = auto()
ALL_ITEMS = auto()

def iterate_samples(self, collection: Collection[T]) -> Collection[T]:
def iterate_samples(self, collection: Iterable[T]) -> Iterable[T]:
if self is CollectionCheckStrategy.FIRST_ITEM:
if len(collection):
try:
return [next(iter(collection))]
else:
except StopIteration:
return ()
else:
return collection
Expand Down
24 changes: 10 additions & 14 deletions metaflow/_vendor/typeguard/_decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,18 @@
from ._transformer import TypeguardTransformer
from ._utils import Unset, function_name, get_stacklevel, is_method_of, unset

T_CallableOrType = TypeVar("T_CallableOrType", bound=Callable[..., Any])

if TYPE_CHECKING:
from typeshed.stdlib.types import _Cell

_F = TypeVar("_F")

def typeguard_ignore(f: _F) -> _F:
def typeguard_ignore(arg: T_CallableOrType) -> T_CallableOrType:
"""This decorator is a noop during static type-checking."""
return f
return arg

else:
from typing import no_type_check as typeguard_ignore # noqa: F401

T_CallableOrType = TypeVar("T_CallableOrType", bound=Callable[..., Any])


def make_cell(value: object) -> _Cell:
return (lambda: value).__closure__[0] # type: ignore[index]
Expand Down Expand Up @@ -133,13 +131,11 @@ def typechecked(
typecheck_fail_callback: TypeCheckFailCallback | Unset = unset,
collection_check_strategy: CollectionCheckStrategy | Unset = unset,
debug_instrumentation: bool | Unset = unset,
) -> Callable[[T_CallableOrType], T_CallableOrType]:
...
) -> Callable[[T_CallableOrType], T_CallableOrType]: ...


@overload
def typechecked(target: T_CallableOrType) -> T_CallableOrType:
...
def typechecked(target: T_CallableOrType) -> T_CallableOrType: ...


def typechecked(
Expand Down Expand Up @@ -215,12 +211,12 @@ def typechecked(
return target

# Find either the first Python wrapper or the actual function
wrapper_class: type[classmethod[Any, Any, Any]] | type[
staticmethod[Any, Any]
] | None = None
wrapper_class: (
type[classmethod[Any, Any, Any]] | type[staticmethod[Any, Any]] | None
) = None
if isinstance(target, (classmethod, staticmethod)):
wrapper_class = target.__class__
target = target.__func__
target = target.__func__ # type: ignore[assignment]

retval = instrument(target)
if isinstance(retval, str):
Expand Down
90 changes: 43 additions & 47 deletions metaflow/_vendor/typeguard/_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

import sys
import warnings
from typing import Any, Callable, NoReturn, TypeVar, overload
from collections.abc import Sequence
from typing import Any, Callable, NoReturn, TypeVar, Union, overload

from . import _suppression
from ._checkers import BINARY_MAGIC_METHODS, check_type_internal
Expand Down Expand Up @@ -32,8 +33,7 @@ def check_type(
forward_ref_policy: ForwardRefPolicy = ...,
typecheck_fail_callback: TypeCheckFailCallback | None = ...,
collection_check_strategy: CollectionCheckStrategy = ...,
) -> T:
...
) -> T: ...


@overload
Expand All @@ -44,16 +44,15 @@ def check_type(
forward_ref_policy: ForwardRefPolicy = ...,
typecheck_fail_callback: TypeCheckFailCallback | None = ...,
collection_check_strategy: CollectionCheckStrategy = ...,
) -> Any:
...
) -> Any: ...


def check_type(
value: object,
expected_type: Any,
*,
forward_ref_policy: ForwardRefPolicy = TypeCheckConfiguration().forward_ref_policy,
typecheck_fail_callback: (TypeCheckFailCallback | None) = (
typecheck_fail_callback: TypeCheckFailCallback | None = (
TypeCheckConfiguration().typecheck_fail_callback
),
collection_check_strategy: CollectionCheckStrategy = (
Expand All @@ -80,7 +79,7 @@ def check_type(
corresponding fields in :class:`TypeCheckConfiguration`.

:param value: value to be checked against ``expected_type``
:param expected_type: a class or generic type instance
:param expected_type: a class or generic type instance, or a tuple of such things
:param forward_ref_policy: see :attr:`TypeCheckConfiguration.forward_ref_policy`
:param typecheck_fail_callback:
see :attr`TypeCheckConfiguration.typecheck_fail_callback`
Expand All @@ -90,6 +89,9 @@ def check_type(
:raises TypeCheckError: if there is a type mismatch

"""
if type(expected_type) is tuple:
expected_type = Union[expected_type]

config = TypeCheckConfiguration(
forward_ref_policy=forward_ref_policy,
typecheck_fail_callback=typecheck_fail_callback,
Expand Down Expand Up @@ -241,59 +243,53 @@ def check_yield_type(


def check_variable_assignment(
value: object, varname: str, annotation: Any, memo: TypeCheckMemo
value: Any, targets: Sequence[list[tuple[str, Any]]], memo: TypeCheckMemo
) -> Any:
if _suppression.type_checks_suppressed:
return

try:
check_type_internal(value, annotation, memo)
except TypeCheckError as exc:
qualname = qualified_name(value, add_class_prefix=True)
exc.append_path_element(f"value assigned to {varname} ({qualname})")
if memo.config.typecheck_fail_callback:
memo.config.typecheck_fail_callback(exc, memo)
else:
raise

return value
return value

value_to_return = value
for target in targets:
star_variable_index = next(
(i for i, (varname, _) in enumerate(target) if varname.startswith("*")),
None,
)
if star_variable_index is not None:
value_to_return = list(value)
remaining_vars = len(target) - 1 - star_variable_index
end_index = len(value_to_return) - remaining_vars
values_to_check = (
value_to_return[:star_variable_index]
+ [value_to_return[star_variable_index:end_index]]
+ value_to_return[end_index:]
)
elif len(target) > 1:
values_to_check = value_to_return = []
iterator = iter(value)
for _ in target:
try:
values_to_check.append(next(iterator))
except StopIteration:
raise ValueError(
f"not enough values to unpack (expected {len(target)}, got "
f"{len(values_to_check)})"
) from None

def check_multi_variable_assignment(
value: Any, targets: list[dict[str, Any]], memo: TypeCheckMemo
) -> Any:
if _suppression.type_checks_suppressed:
return

if max(len(target) for target in targets) == 1:
iterated_values = [value]
else:
iterated_values = list(value)

for expected_types in targets:
value_index = 0
for ann_index, (varname, expected_type) in enumerate(expected_types.items()):
if varname.startswith("*"):
varname = varname[1:]
keys_left = len(expected_types) - 1 - ann_index
next_value_index = len(iterated_values) - keys_left
obj: object = iterated_values[value_index:next_value_index]
value_index = next_value_index
else:
obj = iterated_values[value_index]
value_index += 1
else:
values_to_check = [value]

for val, (varname, annotation) in zip(values_to_check, target):
try:
check_type_internal(obj, expected_type, memo)
check_type_internal(val, annotation, memo)
except TypeCheckError as exc:
qualname = qualified_name(obj, add_class_prefix=True)
qualname = qualified_name(val, add_class_prefix=True)
exc.append_path_element(f"value assigned to {varname} ({qualname})")
if memo.config.typecheck_fail_callback:
memo.config.typecheck_fail_callback(exc, memo)
else:
raise

return iterated_values[0] if len(iterated_values) == 1 else iterated_values
return value_to_return


def warn_on_error(exc: TypeCheckError, memo: TypeCheckMemo) -> None:
Expand Down
4 changes: 2 additions & 2 deletions metaflow/_vendor/typeguard/_importhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
import ast
import sys
import types
from collections.abc import Callable, Iterable
from collections.abc import Callable, Iterable, Sequence
from importlib.abc import MetaPathFinder
from importlib.machinery import ModuleSpec, SourceFileLoader
from importlib.util import cache_from_source, decode_source
from inspect import isclass
from os import PathLike
from types import CodeType, ModuleType, TracebackType
from typing import Sequence, TypeVar
from typing import TypeVar
from unittest.mock import patch

from ._config import global_config
Expand Down
53 changes: 40 additions & 13 deletions metaflow/_vendor/typeguard/_pytest_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,45 @@

import sys
import warnings

from pytest import Config, Parser
from typing import TYPE_CHECKING, Any, Literal

from metaflow._vendor.typeguard._config import CollectionCheckStrategy, ForwardRefPolicy, global_config
from metaflow._vendor.typeguard._exceptions import InstrumentationWarning
from metaflow._vendor.typeguard._importhook import install_import_hook
from metaflow._vendor.typeguard._utils import qualified_name, resolve_reference

if TYPE_CHECKING:
from pytest import Config, Parser


def pytest_addoption(parser: Parser) -> None:
def add_ini_option(
opt_type: (
Literal["string", "paths", "pathlist", "args", "linelist", "bool"] | None
),
) -> None:
parser.addini(
group.options[-1].names()[0][2:],
group.options[-1].attrs()["help"],
opt_type,
)

group = parser.getgroup("typeguard")
group.addoption(
"--typeguard-packages",
action="store",
help="comma separated name list of packages and modules to instrument for "
"type checking, or :all: to instrument all modules loaded after typeguard",
)
add_ini_option("linelist")

group.addoption(
"--typeguard-debug-instrumentation",
action="store_true",
help="print all instrumented code to stderr",
)
add_ini_option("bool")

group.addoption(
"--typeguard-typecheck-fail-callback",
action="store",
Expand All @@ -33,6 +50,8 @@ def pytest_addoption(parser: Parser) -> None:
"handle a TypeCheckError"
),
)
add_ini_option("string")

group.addoption(
"--typeguard-forward-ref-policy",
action="store",
Expand All @@ -42,21 +61,31 @@ def pytest_addoption(parser: Parser) -> None:
"annotations"
),
)
add_ini_option("string")

group.addoption(
"--typeguard-collection-check-strategy",
action="store",
choices=list(CollectionCheckStrategy.__members__),
help="determines how thoroughly to check collections (list, dict, etc)",
)
add_ini_option("string")


def pytest_configure(config: Config) -> None:
packages_option = config.getoption("typeguard_packages")
if packages_option:
if packages_option == ":all:":
packages: list[str] | None = None
def getoption(name: str) -> Any:
return config.getoption(name.replace("-", "_")) or config.getini(name)

packages: list[str] | None = []
if packages_option := config.getoption("typeguard_packages"):
packages = [pkg.strip() for pkg in packages_option.split(",")]
elif packages_ini := config.getini("typeguard-packages"):
packages = packages_ini

if packages:
if packages == [":all:"]:
packages = None
else:
packages = [pkg.strip() for pkg in packages_option.split(",")]
already_imported_packages = sorted(
package for package in packages if package in sys.modules
)
Expand All @@ -70,11 +99,11 @@ def pytest_configure(config: Config) -> None:

install_import_hook(packages=packages)

debug_option = config.getoption("typeguard_debug_instrumentation")
debug_option = getoption("typeguard-debug-instrumentation")
if debug_option:
global_config.debug_instrumentation = True

fail_callback_option = config.getoption("typeguard_typecheck_fail_callback")
fail_callback_option = getoption("typeguard-typecheck-fail-callback")
if fail_callback_option:
callback = resolve_reference(fail_callback_option)
if not callable(callback):
Expand All @@ -85,14 +114,12 @@ def pytest_configure(config: Config) -> None:

global_config.typecheck_fail_callback = callback

forward_ref_policy_option = config.getoption("typeguard_forward_ref_policy")
forward_ref_policy_option = getoption("typeguard-forward-ref-policy")
if forward_ref_policy_option:
forward_ref_policy = ForwardRefPolicy.__members__[forward_ref_policy_option]
global_config.forward_ref_policy = forward_ref_policy

collection_check_strategy_option = config.getoption(
"typeguard_collection_check_strategy"
)
collection_check_strategy_option = getoption("typeguard-collection-check-strategy")
if collection_check_strategy_option:
collection_check_strategy = CollectionCheckStrategy.__members__[
collection_check_strategy_option
Expand Down
8 changes: 3 additions & 5 deletions metaflow/_vendor/typeguard/_suppression.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,15 @@


@overload
def suppress_type_checks(func: Callable[P, T]) -> Callable[P, T]:
...
def suppress_type_checks(func: Callable[P, T]) -> Callable[P, T]: ...


@overload
def suppress_type_checks() -> ContextManager[None]:
...
def suppress_type_checks() -> ContextManager[None]: ...


def suppress_type_checks(
func: Callable[P, T] | None = None
func: Callable[P, T] | None = None,
) -> Callable[P, T] | ContextManager[None]:
"""
Temporarily suppress all type checking.
Expand Down
Loading