def test_is_hint_pep() -> None: ''' Test the :func:`beartype._util.hint.pep.utilhintpeptest.is_hint_pep` tester. ''' # Defer heavyweight imports. from beartype._util.hint.pep.utilhintpeptest import is_hint_pep from beartype_test.a00_unit.data.hint.data_hint import NOT_HINTS_PEP from beartype_test.a00_unit.data.hint.nonpep.data_hintnonpep import ( HINTS_NONPEP_META) from beartype_test.a00_unit.data.hint.pep.data_hintpep import HINTS_PEP_META # Assert this tester accepts PEP-compliant type hints. for hint_pep_meta in HINTS_PEP_META: assert is_hint_pep(hint_pep_meta.hint) is True # Assert this tester rejects PEP-noncompliant type hints implemented by the # "typing" module as normal types indistinguishable from non-"typing" types # and thus effectively non-PEP-compliant for all practical intents. for hint_nonpep_meta in HINTS_NONPEP_META: assert is_hint_pep(hint_nonpep_meta.hint) is False # Assert this tester rejects non-PEP-compliant type hints. for not_hint_pep in NOT_HINTS_PEP: assert is_hint_pep(not_hint_pep) is False
def is_hint_nonpep_tuple( # Mandatory parameters. hint: object, # Optional parameters. is_str_valid: bool = True, ) -> bool: ''' ``True`` only if the passed object is a PEP-noncompliant non-empty tuple of one or more classes. This tester is memoized for efficiency. Parameters ---------- hint : object Object to be inspected. is_str_valid : Optional[bool] ``True`` only if this function permits this object to be a string. Defaults to ``True``. If this boolean is: * ``True``, this object is valid only if this object is a tuple of classes and/or classnames. * ``False``, this object is valid only if this object is a tuple of classes. Returns ---------- bool ``True`` only if this object is a **non-empty tuple** (i.e., semantic union of types) containing one or more: * Non-:mod:`typing` types. * If ``is_str_valid``, **strings** (i.e., forward references specified as either fully-qualified or unqualified classnames). ''' assert isinstance(is_str_valid, bool), f'{repr(is_str_valid)} not boolean.' #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! # BEGIN: Synchronize changes here with die_unless_hint_nonpep() above. #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! # Avoid circular import dependencies. from beartype._util.hint.pep.utilhintpeptest import is_hint_pep # Return true only if this object is... return ( # A tuple *AND*... isinstance(hint, tuple) and # This tuple is non-empty *AND*... len(hint) > 0 and # Each item of this tuple is either a caller-permitted forward # reference *OR* a PEP-noncompliant class. all( not is_hint_pep(hint_item) if isinstance(hint_item, type) else is_str_valid if isinstance(hint_item, str) else False for hint_item in hint ) )
def _is_hint_nonpep_type(hint: object) -> bool: ''' ``True`` only if the passed object is a PEP-noncompliant type. This tester is intentionally *not* memoized (e.g., by the :func:`callable_cached` decorator), as the implementation trivially reduces to an efficient one-liner. Parameters ---------- hint : object Object to be inspected. Returns ---------- bool ``True`` only if this object is a PEP-noncompliant type. ''' #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! # BEGIN: Synchronize changes here with die_unless_hint_nonpep() above. #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! # Avoid circular import dependencies. from beartype._util.hint.pep.utilhintpeptest import is_hint_pep # Return true only if this object is isinstanceable and *NOT* a # PEP-compliant class, in which case this *MUST* be a PEP-noncompliant # class by definition. return is_type_isinstanceable(hint) and not is_hint_pep(hint)
def is_hint_ignorable(hint: object) -> bool: ''' ``True`` only if the passed object is an **ignorable type hint.** This tester function is memoized for efficiency. Parameters ---------- hint : object Object to be inspected. Returns ---------- bool ``True`` only if this object is an ignorable type hint. Raises ---------- TypeError If this object is **unhashable** (i.e., *not* hashable by the builtin :func:`hash` function and thus unusable in hash-based containers like dictionaries and sets). All supported type hints are hashable. ''' # Attempt to... try: # If this hint is shallowly ignorable, return true. if hint in HINTS_IGNORABLE_SHALLOW: return True # Else, this hint is *NOT* shallowly ignorable. # If this hint is unhashable, hint is *NOT* shallowly ignorable. except TypeError: pass # If this hint is PEP-compliant... if is_hint_pep(hint): # Avoid circular import dependencies. from beartype._util.hint.pep.utilhintpeptest import ( is_hint_pep_ignorable) # Defer to the function testing whether this hint is an ignorable # PEP-compliant type hint. return is_hint_pep_ignorable(hint) # Else, this hint is PEP-noncompliant and thus *NOT* deeply ignorable. # Since this hint is also *NOT* shallowly ignorable, this hint is # unignorable. In this case, return false. return False
def is_hint(hint: object) -> bool: ''' ``True`` only if the passed object is a **supported type hint** (i.e., object supported by the :func:`beartype.beartype` decorator as a valid type hint annotating callable parameters and return values). This tester function is memoized for efficiency. Parameters ---------- hint : object Object to be validated. Returns ---------- bool ``True`` only if this object is either: * A **PEP-compliant type hint** (i.e., :mod:`beartype`-agnostic annotation compliant with annotation-centric PEPs). * A **PEP-noncompliant type hint** (i.e., :mod:`beartype`-specific annotation intentionally *not* compliant with annotation-centric PEPs). Raises ---------- TypeError If this object is **unhashable** (i.e., *not* hashable by the builtin :func:`hash` function and thus unusable in hash-based containers like dictionaries and sets). All supported type hints are hashable. ''' #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! # BEGIN: Synchronize changes here with die_unless_hint() above. #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! # Return true only if... return ( # This is a PEP-compliant type hint supported by @beartype *OR*... is_hint_pep_supported(hint) if is_hint_pep(hint) else # This is a PEP-noncompliant type hint, which by definition is # necessarily supported by @beartype. is_hint_nonpep(hint) )
def __init__( self, func: object, pith: object, hint: object, cause_indent: str, exception_label: str, ) -> None: ''' Initialize this object. ''' assert callable(func), f'{repr(func)} not callable.' assert isinstance(cause_indent, str), (f'{repr(cause_indent)} not string.') assert isinstance(exception_label, str), (f'{repr(exception_label)} not string.') # Classify all passed parameters. self.func = func self.pith = pith self.hint = hint self.cause_indent = cause_indent self.exception_label = exception_label # Nullify all remaining parameters for safety. self.hint_sign = None self.hint_childs = None # ................{ REDUCTION }................ #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! # CAVEATS: Synchronize changes here with the corresponding block of the # beartype._decor._code._pep._pephint.pep_code_check_hint() function. #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! # This logic reduces the currently visited hint to an arbitrary object # associated with this hint when this hint conditionally satisfies any # of various conditions. # # ................{ REDUCTION ~ pep 484 }................ # If this is a PEP 484-compliant new type hint, reduce this hint to the # user-defined class aliased by this hint. Although this logic could # also be performed below, doing so here simplifies matters. if is_hint_pep484_newtype(self.hint): self.hint = get_hint_pep484_newtype_class(self.hint) # ................{ REDUCTION ~ pep 544 }................ # If this is a PEP 484-compliant IO generic base class *AND* the active # Python interpreter targets at least Python >= 3.8 and thus supports # PEP 544-compliant protocols, reduce this functionally useless hint to # the corresponding functionally useful beartype-specific PEP # 544-compliant protocol implementing this hint. elif is_hint_pep544_io_generic(self.hint): self.hint = get_hint_pep544_io_protocol_from_generic(self.hint) # ................{ REDUCTION ~ pep 593 }................ # If this is a PEP 593-compliant type metahint, ignore all annotations # on this hint (i.e., "hint_curr.__metadata__" tuple) by reducing this # hint to its origin (e.g., "str" in "Annotated[str, 50, False]"). elif is_hint_pep593(self.hint): self.hint = get_hint_pep593_hint(self.hint) # ................{ REDUCTION ~ end }................ # If this hint is PEP-compliant... if is_hint_pep(self.hint): # Arbitrary object uniquely identifying this hint. self.hint_sign = get_hint_pep_sign(self.hint) # Tuple of either... self.hint_childs = ( # If this hint is a generic, the one or more unerased # pseudo-superclasses originally subclassed by this hint. get_hint_pep_generic_bases_unerased(self.hint) if is_hint_pep_generic(self.hint) else # Else, the zero or more arguments subscripting this hint. get_hint_pep_args(self.hint))
def get_cause_or_none_union(sleuth: CauseSleuth) -> 'Optional[str]': ''' Human-readable string describing the failure of the passed arbitrary object to satisfy the passed PEP-compliant union type hint if this object actually fails to satisfy this hint *or* ``None`` otherwise (i.e., if this object satisfies this hint). Parameters ---------- sleuth : CauseSleuth Type-checking error cause sleuth. ''' assert isinstance(sleuth, CauseSleuth), f'{repr(sleuth)} not cause sleuth.' assert sleuth.hint_sign in HINT_PEP484_SIGNS_UNION, ( f'{repr(sleuth.hint)} not union.') # Subset of all classes shallowly associated with these child hints (i.e., # by being either these child hints in the case of non-"typing" classes # *OR* the classes originating these child hints in the case of # PEP-compliant type hints) that this pith fails to shallowly satisfy. hint_classes_unsatisfied = set() # List of all human-readable strings describing the failure of this pith to # satisfy each of these child hints. causes_union = [] # Indentation preceding each line of the strings returned by child getter # functions called by this parent getter function, offset to visually # demarcate child from parent causes in multiline strings. CAUSE_INDENT_CHILD = sleuth.cause_indent + ' ' # For each subscripted argument of this union... for hint_child in sleuth.hint_childs: # If this child hint is ignorable, continue to the next. if is_hint_ignorable(hint_child): continue # Else, this child hint is unignorable. # If this child hint is PEP-compliant... if is_hint_pep(hint_child): # Non-"typing" class originating this child hint if any *OR* "None" # otherwise. hint_child_type_origin = get_hint_pep_type_origin_or_none( hint_child) # If... if ( # This child hint originates from a non-"typing" class *AND*... hint_child_type_origin is not None and # This pith is *NOT* an instance of this class... not isinstance(sleuth.pith, hint_child_type_origin) # Then this pith fails to satisfy this child hint. In this case... ): # Add this class to the subset of all classes this pith does # *NOT* satisfy. hint_classes_unsatisfied.add(hint_child_type_origin) # Continue to the next child hint. continue # Else, this pith is an instance of this class and thus shallowly # (but *NOT* necessarily deeply) satisfies this child hint. # Human-readable string describing the failure of this pith to # deeply satisfy this child hint if this pith actually fails to # deeply satisfy this child hint *or* "None" otherwise. pith_cause_hint_child = sleuth.permute( hint=hint_child, cause_indent=CAUSE_INDENT_CHILD, ).get_cause_or_none() # If this pith deeply satisfies this child hint, return "None". if pith_cause_hint_child is None: # print('Union child {!r} pith {!r} deeply satisfied!'.format(hint_child, pith)) return None # Else, this pith does *NOT* deeply satisfy this child hint. # Append a cause as a discrete bullet-prefixed line. causes_union.append(pith_cause_hint_child) # Else, this child hint is PEP-noncompliant. In this case... else: # Assert this child hint to be a non-"typing" class. Note that # the "typing" module should have already guaranteed that all # subscripted arguments of unions are either PEP-compliant type # hints or non-"typing" classes. assert isinstance(hint_child, type), ( f'{sleuth.exception_label} PEP union type hint ' f'{repr(sleuth.hint)} child hint {repr(hint_child)} invalid ' f'(i.e., neither PEP type hint nor non-"typing" class).') # Else, this child hint is a non-"typing" type. # If this pith is an instance of this class, this pith satisfies # this hint. In this case, return "None". if isinstance(sleuth.pith, hint_child): return None # Else, this pith is *NOT* an instance of this class, implying this # pith to *NOT* satisfy this hint. In this case, add this class to # the subset of all classes this pith does *NOT* satisfy. hint_classes_unsatisfied.add(hint_child) # If this pith fails to shallowly satisfy one or more classes, concatenate # these failures onto a discrete bullet-prefixed line. if hint_classes_unsatisfied: # Human-readable comma-delimited disjunction of the names of these # classes (e.g., "bool, float, int, or str"). cause_types_unsatisfied = join_delimited_disjunction_classes( hint_classes_unsatisfied) # Prepend this cause as a discrete bullet-prefixed line. # # Note that this cause is intentionally prependend rather than appended # to this list. Since this cause applies *ONLY* to the shallow type of # the current pith rather than any items contained in this pith, # listing this shallow cause *BEFORE* other deeper causes typically # applying to items contained in this pith produces substantially more # human-readable exception messages: e.g., # # This reads well. # @beartyped pep_hinted() parameter pep_hinted_param=(1,) violates # PEP type hint typing.Union[int, typing.Sequence[str]], as (1,): # * Not int. # * Tuple item 0 value "1" not str. # # # This does not. # @beartyped pep_hinted() parameter pep_hinted_param=(1,) violates # PEP type hint typing.Union[int, typing.Sequence[str]], as (1,): # * Tuple item 0 value "1" not str. # * Not int. # # Note that prepending to lists is an O(n) operation, but that this # cost is negligible in this case both due to the negligible number of # child hints of the average "typing.Union" in general *AND* due to the # fact that this function is only called when a catastrophic type-check # failure has already occurred. causes_union.insert(0, f'not {cause_types_unsatisfied}') # If prior logic appended *NO* causes, raise an exception. if not causes_union: raise _BeartypeCallHintPepRaiseException( f'{sleuth.exception_label} PEP type hint ' f'{repr(sleuth.hint)} failure causes unknown.') # Else, prior logic appended one or more strings describing these failures. # Truncated object representation of this pith. pith_repr = get_object_representation(sleuth.pith) # If prior logic appended one cause, return this cause as a single-line # substring intended to be embedded in a longer string. if len(causes_union) == 1: return f'{pith_repr} {causes_union[0]}' # Else, prior logic appended two or more causes. # Return a multiline string comprised of... return '{}:\n{}'.format( # This truncated object representation. pith_repr, # The newline-delimited concatenation of each cause as a discrete # bullet-prefixed line... '\n'.join( '{}* {}'.format( # Indented by the current indent. sleuth.cause_indent, # Whose first character is uppercased. uppercase_char_first( # Suffixed by a period if *NOT* yet suffixed by a period. suffix_unless_suffixed(text=cause_union, suffix='.'))) # '{}* {}.'.format(cause_indent, uppercase_char_first(cause_union)) for cause_union in causes_union))
def is_hint_nonpep( # Mandatory parameters. hint: object, # Optional parameters. is_str_valid: bool = True, ) -> bool: ''' ``True`` only if the passed object is a **PEP-noncompliant type hint** (i.e., :mod:`beartype`-specific annotation *not* compliant with annotation-centric PEPs). This tester is intentionally *not* memoized (e.g., by the :func:`callable_cached` decorator), as the implementation trivially reduces to an efficient one-liner. Parameters ---------- hint : object Object to be inspected. is_str_valid : Optional[bool] ``True`` only if this function permits this object to be a string. Defaults to ``True``. If this boolean is: * ``True``, this object is valid only if this object is either a class or tuple of classes and/or classnames. * ``False``, this object is valid only if this object is either a class or tuple of classes. Returns ---------- bool ``True`` only if this object is either: * A non-:mod:`typing` type (i.e., class *not* defined by the :mod:`typing` module, whose public classes are used to instantiate PEP-compliant type hints or objects satisfying such hints that typically violate standard class semantics and thus require PEP-specific handling). * A **non-empty tuple** (i.e., semantic union of types) containing one or more: * Non-:mod:`typing` types. * If ``is_str_valid``, **strings** (i.e., forward references specified as either fully-qualified or unqualified classnames). ''' assert isinstance(is_str_valid, bool), f'{repr(is_str_valid)} not boolean.' #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! # BEGIN: Synchronize changes here with die_unless_hint_nonpep() above. #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! # Avoid circular import dependencies. from beartype._util.hint.pep.utilhintpeptest import is_hint_pep # Return true only if either... return ( # If this object is a class, return true only if this is *NOT* a # PEP-compliant class, in which case this *MUST* be a PEP-noncompliant # class by definition. not is_hint_pep(hint) if isinstance(hint, type) else # Else, this object is *NOT* a class. # # If this object is a tuple, return true only if this tuple contains # only one or more caller-permitted forward references and # PEP-noncompliant classes. is_hint_nonpep_tuple(hint, is_str_valid) if isinstance(hint, tuple) # Else, this object is neither a class nor tuple. Return false, as this # object *CANNOT* be PEP-noncompliant. else False )
def hint(self, hint: Any) -> None: ''' Set the type hint to validate this object against. ''' # ................{ REDUCTION }................ #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! # CAVEATS: Synchronize changes here with the corresponding block of the # beartype._decor._code._pep._pephint.pep_code_check_hint() function. #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! # # This logic reduces the currently visited hint to an arbitrary object # associated with this hint when this hint conditionally satisfies any # of various conditions. # # ................{ REDUCTION ~ pep 484 }................ # If this is the PEP 484-compliant "None" singleton, reduce this hint # to the type of that singleton. While not explicitly defined by the # "typing" module, PEP 484 explicitly supports this singleton: # When used in a type hint, the expression None is considered # equivalent to type(None). if hint is None: hint = NoneType # If this is a PEP 484-compliant new type hint, reduce this hint to the # user-defined class aliased by this hint. Although this logic could # also be performed below, doing so here simplifies matters. elif is_hint_pep484_newtype(hint): hint = get_hint_pep484_newtype_class(hint) # ................{ REDUCTION ~ pep 544 }................ # If this is a PEP 484-compliant IO generic base class *AND* the active # Python interpreter targets at least Python >= 3.8 and thus supports # PEP 544-compliant protocols, reduce this functionally useless hint to # the corresponding functionally useful beartype-specific PEP # 544-compliant protocol implementing this hint. # # Note that PEP 484-compliant IO generic base classes are technically # usable under Python < 3.8 (e.g., by explicitly subclassing those # classes from third-party classes). Ergo, we can neither safely emit # warnings nor raise exceptions on visiting these classes under *ANY* # Python version. elif is_hint_pep544_io_generic(hint): hint = get_hint_pep544_io_protocol_from_generic(hint) # ................{ REDUCTION ~ pep 593 }................ # If this is a PEP 593-compliant type metahint, ignore all annotations # on this hint (i.e., "hint_curr.__metadata__" tuple) by reducing this # hint to its origin (e.g., "str" in "Annotated[str, 50, False]"). elif is_hint_pep593(hint): hint = get_hint_pep593_hint(hint) # ................{ REDUCTION ~ end }................ # If this hint is PEP-compliant... if is_hint_pep(hint): # Arbitrary object uniquely identifying this hint. self.hint_sign = get_hint_pep_sign(hint) # Tuple of either... self.hint_childs = ( # If this hint is a generic, the one or more unerased # pseudo-superclasses originally subclassed by this hint. get_hint_pep_generic_bases_unerased(hint) if is_hint_pep_generic(hint) else # Else, the zero or more arguments subscripting this hint. get_hint_pep_args(hint)) # Classify this hint *AFTER* all other assignments above. self._hint = hint
def die_unless_hint( # Mandatory parameters. hint: object, # Optional parameters. hint_label: str = 'Annotated', ) -> None: ''' Raise an exception unless the passed object is a **supported type hint** (i.e., object supported by the :func:`beartype.beartype` decorator as a valid type hint annotating callable parameters and return values). Specifically, this function raises an exception if this object is neither: * A **supported PEP-compliant type hint** (i.e., :mod:`beartype`-agnostic annotation compliant with annotation-centric PEPs currently supported by the :func:`beartype.beartype` decorator). * A **PEP-noncompliant type hint** (i.e., :mod:`beartype`-specific annotation intentionally *not* compliant with annotation-centric PEPs). Efficiency ---------- This validator is effectively (but technically *not*) memoized. Since the passed ``hint_label`` parameter is typically unique to each call to this validator, memoizing this validator would uselessly consume excess space *without* improving time efficiency. Instead, this validator first calls the memoized :func:`is_hint_pep` tester. If that tester returns ``True``, this validator immediately returns ``True`` and is thus effectively memoized; else, this validator inefficiently raises a human-readable exception without memoization. Since efficiency is largely irrelevant in exception handling, this validator thus remains effectively memoized. Parameters ---------- hint : object Object to be validated. hint_label : Optional[str] Human-readable label prefixing this object's representation in the exception message raised by this function. Defaults to ``"Annotated"``. Raises ---------- BeartypeDecorHintPepUnsupportedException If this object is a PEP-compliant type hint currently unsupported by the :func:`beartype.beartype` decorator. BeartypeDecorHintNonPepException If this object is neither: * A PEP-noncompliant type hint. * A supported PEP-compliant type hint. ''' # If this object is a supported type hint, reduce to a noop. if is_hint(hint): return # Else, this object is *NOT* a supported type hint. In this case, # subsequent logic raises an exception specific to the passed parameters. #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! # BEGIN: Synchronize changes here with is_hint() below. #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! # If this hint is PEP-compliant, raise an exception only if this hint is # currently unsupported by @beartype. if is_hint_pep(hint): die_if_hint_pep_unsupported( hint=hint, hint_label=hint_label, ) # Else, this hint is *NOT* PEP-compliant. In this case, raise an exception # only if this hint is also *NOT* PEP-noncompliant. By definition, all # PEP-noncompliant type hints are supported by @beartype. die_unless_hint_nonpep( hint=hint, hint_label=hint_label, )