Skip to content

Commit

Permalink
Merge pull request #4252 from tybug/more-typing
Browse files Browse the repository at this point in the history
Add more type hints
  • Loading branch information
tybug authored Jan 25, 2025
2 parents c12cce7 + 3c32382 commit 93fe3f7
Show file tree
Hide file tree
Showing 17 changed files with 283 additions and 199 deletions.
3 changes: 3 additions & 0 deletions hypothesis-python/RELEASE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
RELEASE_TYPE: patch

More work on internal type hints.
85 changes: 50 additions & 35 deletions hypothesis-python/src/hypothesis/_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,18 @@
import inspect
import os
import warnings
from collections.abc import Collection
from collections.abc import Collection, Generator, Sequence
from enum import Enum, EnumMeta, IntEnum, unique
from typing import TYPE_CHECKING, Any, ClassVar, Optional, TypeVar, Union
from typing import (
TYPE_CHECKING,
Any,
Callable,
ClassVar,
NoReturn,
Optional,
TypeVar,
Union,
)

import attr

Expand All @@ -36,17 +45,20 @@
from hypothesis.utils.dynamicvariables import DynamicVariable

if TYPE_CHECKING:
from typing import TypeAlias

from hypothesis.database import ExampleDatabase

__all__ = ["settings"]

ValidatorT: "TypeAlias" = Callable[[Any], object]
all_settings: dict[str, "Setting"] = {}

T = TypeVar("T")


class settingsProperty:
def __init__(self, name, show_default):
def __init__(self, name: str, *, show_default: bool) -> None:
self.name = name
self.show_default = show_default

Expand Down Expand Up @@ -85,27 +97,28 @@ def __doc__(self):
return f"{description}\n\ndefault value: ``{default}``"


default_variable = DynamicVariable(None)
default_variable = DynamicVariable[Optional["settings"]](None)


class settingsMeta(type):
def __init__(cls, *args, **kwargs):
super().__init__(*args, **kwargs)

@property
def default(cls):
def default(cls) -> Optional["settings"]:
v = default_variable.value
if v is not None:
return v
if getattr(settings, "_current_profile", None) is not None:
assert settings._current_profile is not None
settings.load_profile(settings._current_profile)
assert default_variable.value is not None
return default_variable.value

def _assign_default_internal(cls, value):
def _assign_default_internal(cls, value: "settings") -> None:
default_variable.value = value

def __setattr__(cls, name, value):
def __setattr__(cls, name: str, value: object) -> None:
if name == "default":
raise AttributeError(
"Cannot assign to the property settings.default - "
Expand All @@ -118,7 +131,7 @@ def __setattr__(cls, name, value):
"settings with settings.load_profile, or use @settings(...) "
"to decorate your test instead."
)
return super().__setattr__(name, value)
super().__setattr__(name, value)


class settings(metaclass=settingsMeta):
Expand Down Expand Up @@ -233,14 +246,14 @@ def __call__(self, test: T) -> T:
@classmethod
def _define_setting(
cls,
name,
description,
name: str,
description: str,
*,
default,
options=None,
validator=None,
show_default=True,
):
default: object,
options: Optional[Sequence[object]] = None,
validator: Optional[ValidatorT] = None,
show_default: bool = True,
) -> None:
"""Add a new setting.
- name is the name of the property that will be used to access the
Expand Down Expand Up @@ -273,16 +286,16 @@ def validator(value):
default=default,
validator=validator,
)
setattr(settings, name, settingsProperty(name, show_default))
setattr(settings, name, settingsProperty(name, show_default=show_default))

@classmethod
def lock_further_definitions(cls):
def lock_further_definitions(cls) -> None:
settings.__definitions_are_locked = True

def __setattr__(self, name, value):
def __setattr__(self, name: str, value: object) -> NoReturn:
raise AttributeError("settings objects are immutable")

def __repr__(self):
def __repr__(self) -> str:
from hypothesis.internal.conjecture.data import AVAILABLE_PROVIDERS

bits = sorted(
Expand All @@ -292,7 +305,7 @@ def __repr__(self):
)
return "settings({})".format(", ".join(bits))

def show_changed(self):
def show_changed(self) -> str:
bits = []
for name, setting in all_settings.items():
value = getattr(self, name)
Expand Down Expand Up @@ -350,20 +363,20 @@ def load_profile(name: str) -> None:


@contextlib.contextmanager
def local_settings(s):
def local_settings(s: settings) -> Generator[settings, None, None]:
with default_variable.with_value(s):
yield s


@attr.s()
class Setting:
name = attr.ib()
description = attr.ib()
default = attr.ib()
validator = attr.ib()
name: str = attr.ib()
description: str = attr.ib()
default: object = attr.ib()
validator: ValidatorT = attr.ib()


def _max_examples_validator(x):
def _max_examples_validator(x: int) -> int:
check_type(int, x, name="max_examples")
if x < 1:
raise InvalidArgument(
Expand Down Expand Up @@ -421,7 +434,7 @@ def _max_examples_validator(x):
)


def _validate_database(db):
def _validate_database(db: "ExampleDatabase") -> "ExampleDatabase":
from hypothesis.database import ExampleDatabase

if db is None or isinstance(db, ExampleDatabase):
Expand Down Expand Up @@ -459,7 +472,7 @@ class Phase(IntEnum):
shrink = 4 #: controls whether examples will be shrunk.
explain = 5 #: controls whether Hypothesis attempts to explain test failures.

def __repr__(self):
def __repr__(self) -> str:
return f"Phase.{self.name}"


Expand All @@ -476,7 +489,7 @@ class HealthCheck(Enum, metaclass=HealthCheckMeta):
Each member of this enum is a type of health check to suppress.
"""

def __repr__(self):
def __repr__(self) -> str:
return f"{self.__class__.__name__}.{self.name}"

@classmethod
Expand Down Expand Up @@ -557,7 +570,7 @@ class Verbosity(IntEnum):
verbose = 2
debug = 3

def __repr__(self):
def __repr__(self) -> str:
return f"Verbosity.{self.name}"


Expand All @@ -569,7 +582,7 @@ def __repr__(self):
)


def _validate_phases(phases):
def _validate_phases(phases: Sequence[Phase]) -> Sequence[Phase]:
phases = tuple(phases)
for a in phases:
if not isinstance(a, Phase):
Expand All @@ -588,7 +601,7 @@ def _validate_phases(phases):
)


def _validate_stateful_step_count(x):
def _validate_stateful_step_count(x: int) -> int:
check_type(int, x, name="stateful_step_count")
if x < 1:
raise InvalidArgument(f"stateful_step_count={x!r} must be at least one.")
Expand Down Expand Up @@ -646,12 +659,14 @@ def validate_health_check_suppressions(suppressions):
class duration(datetime.timedelta):
"""A timedelta specifically measured in milliseconds."""

def __repr__(self):
def __repr__(self) -> str:
ms = self.total_seconds() * 1000
return f"timedelta(milliseconds={int(ms) if ms == int(ms) else ms!r})"


def _validate_deadline(x):
def _validate_deadline(
x: Union[int, float, datetime.timedelta, None]
) -> Optional[duration]:
if x is None:
return x
invalid_deadline_error = InvalidArgument(
Expand Down Expand Up @@ -715,7 +730,7 @@ def is_in_ci() -> bool:
)


def _backend_validator(value):
def _backend_validator(value: str) -> str:
from hypothesis.internal.conjecture.data import AVAILABLE_PROVIDERS

if value not in AVAILABLE_PROVIDERS:
Expand Down
6 changes: 4 additions & 2 deletions hypothesis-python/src/hypothesis/control.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from hypothesis.internal.validation import check_type
from hypothesis.reporting import report, verbose_report
from hypothesis.utils.dynamicvariables import DynamicVariable
from hypothesis.vendor.pretty import IDKey, pretty
from hypothesis.vendor.pretty import IDKey, PrettyPrintFunction, pretty


def _calling_function_location(what: str, frame: Any) -> str:
Expand Down Expand Up @@ -136,7 +136,9 @@ def __init__(self, data, *, is_final=False, close_on_capture=True):
# Use defaultdict(list) here to handle the possibility of having multiple
# functions registered for the same object (due to caching, small ints, etc).
# The printer will discard duplicates which return different representations.
self.known_object_printers = defaultdict(list)
self.known_object_printers: dict[IDKey, list[PrettyPrintFunction]] = (
defaultdict(list)
)

def record_call(self, obj, func, args, kwargs):
self.known_object_printers[IDKey(obj)].append(
Expand Down
4 changes: 4 additions & 0 deletions hypothesis-python/src/hypothesis/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1132,6 +1132,10 @@ def _execute_once_for_engine(self, data: ConjectureData) -> None:
# - there's no need to handle hierarchical groups here, at least if no
# such implicit wrapping happens inside hypothesis code (we only care
# about the hypothesis-or-not distinction).
#
# 01-25-2025: this was patched to give the correct
# stacktrace in cpython https://github.com/python/cpython/issues/128799.
# can remove once python3.11 is EOL.
tb = e.exceptions[0].__traceback__ or e.__traceback__
else:
tb = e.__traceback__
Expand Down
32 changes: 20 additions & 12 deletions hypothesis-python/src/hypothesis/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,19 @@
from datetime import datetime, timedelta, timezone
from functools import lru_cache
from hashlib import sha384
from os import getenv
from os import PathLike, getenv
from pathlib import Path, PurePath
from queue import Queue
from threading import Thread
from typing import Optional
from typing import TYPE_CHECKING, Any, Literal, Optional, Union, cast
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
from zipfile import BadZipFile, ZipFile

from hypothesis.configuration import storage_directory
from hypothesis.errors import HypothesisException, HypothesisWarning
from hypothesis.internal.conjecture.choice import ChoiceT
from hypothesis.utils.conventions import not_set
from hypothesis.utils.conventions import UniqueIdentifier, not_set

__all__ = [
"DirectoryBasedExampleDatabase",
Expand All @@ -43,8 +43,13 @@
"ReadOnlyDatabase",
]

if TYPE_CHECKING:
from typing import TypeAlias

def _usable_dir(path: os.PathLike) -> bool:
StrPathT: "TypeAlias" = Union[str, PathLike[str]]


def _usable_dir(path: StrPathT) -> bool:
"""
Returns True if the desired path can be used as database path because
either the directory exists and can be used, or its root directory can
Expand All @@ -60,7 +65,9 @@ def _usable_dir(path: os.PathLike) -> bool:
return False


def _db_for_path(path=None):
def _db_for_path(
path: Optional[Union[StrPathT, UniqueIdentifier, Literal[":memory:"]]] = None
) -> "ExampleDatabase":
if path is not_set:
if os.getenv("HYPOTHESIS_DATABASE_FILE") is not None: # pragma: no cover
raise HypothesisException(
Expand All @@ -81,11 +88,12 @@ def _db_for_path(path=None):
return InMemoryExampleDatabase()
if path in (None, ":memory:"):
return InMemoryExampleDatabase()
path = cast(StrPathT, path)
return DirectoryBasedExampleDatabase(path)


class _EDMeta(abc.ABCMeta):
def __call__(self, *args, **kwargs):
def __call__(self, *args: Any, **kwargs: Any) -> "ExampleDatabase":
if self is ExampleDatabase:
return _db_for_path(*args, **kwargs)
return super().__call__(*args, **kwargs)
Expand Down Expand Up @@ -160,8 +168,8 @@ class InMemoryExampleDatabase(ExampleDatabase):
does not persist between runs we do not recommend it for general use.
"""

def __init__(self):
self.data = {}
def __init__(self) -> None:
self.data: dict[bytes, set[bytes]] = {}

def __repr__(self) -> str:
return f"InMemoryExampleDatabase({self.data!r})"
Expand All @@ -176,7 +184,7 @@ def delete(self, key: bytes, value: bytes) -> None:
self.data.get(key, set()).discard(bytes(value))


def _hash(key):
def _hash(key: bytes) -> str:
return sha384(key).hexdigest()[:16]


Expand All @@ -199,7 +207,7 @@ class DirectoryBasedExampleDatabase(ExampleDatabase):
the :class:`~hypothesis.database.MultiplexedDatabase` helper.
"""

def __init__(self, path: os.PathLike) -> None:
def __init__(self, path: StrPathT) -> None:
self.path = Path(path)
self.keypaths: dict[bytes, Path] = {}

Expand All @@ -214,7 +222,7 @@ def _key_path(self, key: bytes) -> Path:
self.keypaths[key] = self.path / _hash(key)
return self.keypaths[key]

def _value_path(self, key, value):
def _value_path(self, key: bytes, value: bytes) -> Path:
return self._key_path(key) / _hash(value)

def fetch(self, key: bytes) -> Iterable[bytes]:
Expand Down Expand Up @@ -429,7 +437,7 @@ def __init__(
repo: str,
artifact_name: str = "hypothesis-example-db",
cache_timeout: timedelta = timedelta(days=1),
path: Optional[os.PathLike] = None,
path: Optional[StrPathT] = None,
):
self.owner = owner
self.repo = repo
Expand Down
Loading

0 comments on commit 93fe3f7

Please sign in to comment.