def get_cause_or_none_type_origin(sleuth: CauseSleuth) -> Optional[str]: ''' Human-readable string describing the failure of the passed arbitrary object to satisfy the passed **PEP-compliant originative type hint** (i.e., PEP-compliant type hint originating from a non-:mod:`typing` class) 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.' # Origin type originating this hint if any *OR* "None" otherwise. hint_type_origin = get_hint_pep_stdlib_type_or_none(sleuth.hint) # If this hint does *NOT* originate from such a type, raise an exception. if hint_type_origin is None: raise _BeartypeCallHintPepRaiseException( f'{sleuth.exception_label} type hint ' f'{repr(sleuth.hint)} not originated from an origin type.' ) # Else, this hint originates from such a type. # Defer to the getter function handling non-"typing" classes. Presto! return get_cause_or_none_type(sleuth.permute(hint=hint_type_origin))
def get_cause_or_none_type(sleuth: CauseSleuth) -> Optional[str]: ''' Human-readable string describing the failure of the passed arbitrary object to be an instance of the passed non-:mod:`typing` class if this object is *not* an instance of this class *or* ``None`` otherwise (i.e., if this object is an instance of this class). Parameters ---------- sleuth : CauseSleuth Type-checking error cause sleuth. ''' assert isinstance(sleuth, CauseSleuth), f'{repr(sleuth)} not cause sleuth.' # If this hint is *NOT* a class, raise an exception. if not isinstance(sleuth.hint, type): raise _BeartypeCallHintPepRaiseException( f'{sleuth.exception_label} non-PEP type hint ' f'{repr(sleuth.hint)} unsupported ' f'(i.e., neither PEP-compliant nor standard class).' ) # Else, this hint is a class. # # If this pith is an instance of this class, return "None". elif isinstance(sleuth.pith, sleuth.hint): return None # Else, this pith is *NOT* an instance of this type. # Return a substring describing this failure intended to be embedded in a # longer string. return get_cause_object_not_type(pith=sleuth.pith, hint=sleuth.hint)
def permute(self, **kwargs) -> 'CauseSleuth': ''' Shallow copy of this object such that each the passed keyword argument overwrites the instance variable of the same name in this copy. Parameters ---------- Keyword arguments of the same name and type as instance variables of this object (e.g., ``hint``, ``pith``). Returns ---------- CauseSleuth Shallow copy of this object such that each keyword argument overwrites the instance variable of the same name in this copy. Raises ---------- _BeartypeCallHintPepRaiseException If the name of any passed keyword argument is *not* the name of an existing instance variable of this object. Examples ---------- >>> sleuth = CauseSleuth( ... pith=[42,] ... hint=typing.List[int], ... cause_indent='', ... exception_label='List of integers', ... ) >>> sleuth_copy = sleuth.permute(pith=[24,]) >>> sleuth_copy.pith [24,] >>> sleuth_copy.hint typing.List[int] ''' # For the name of each passed keyword argument... for param_name in kwargs.keys(): # If this copy does *NOT* already define an instance variable of # the same name, raise an exception. if param_name not in self._VAR_NAMES: raise _BeartypeCallHintPepRaiseException( f'Unrecognized instance variable ' f'{self.__class__.__name__}.{param_name} not permutable.') # For the name of each instance variable initializable by this class... for param_name in self._INIT_PARAM_NAMES: # If this variable is not already defined by these arguments, # cascade the current value of this variable into these arguments. if param_name not in kwargs: kwargs[param_name] = getattr(self, param_name) # Return a new instance of this class initialized with these arguments. return CauseSleuth(**kwargs)
def permute(self, **kwargs) -> 'CauseSleuth': ''' Shallow copy of this object such that each the passed keyword argument overwrites the instance variable of the same name in this copy. Parameters ---------- Keyword arguments of the same name and type as instance variables of this object (e.g., ``hint``, ``pith``). Returns ---------- CauseSleuth Shallow copy of this object such that each keyword argument overwrites the instance variable of the same name in this copy. Raises ---------- _BeartypeCallHintPepRaiseException If the name of any passed keyword argument is *not* the name of an existing instance variable of this object. Examples ---------- >>> sleuth = CauseSleuth( ... pith=[42,] ... hint=typing.List[int], ... cause_indent='', ... exception_label='List of integers', ... ) >>> sleuth_copy = sleuth.permute(pith=[24,]) >>> sleuth_copy.pith [24,] >>> sleuth_copy.hint typing.List[int] ''' # For the name of each passed keyword argument... for param_name in kwargs.keys(): # If this name is *NOT* that of a parameter accepted by the # __init__() method, raise an exception. if param_name not in self._INIT_PARAM_NAMES: raise _BeartypeCallHintPepRaiseException( f'{self.__class__}.__init__() parameter ' f'{param_name} unrecognized.') # For the name of each parameter accepted by the __init__() method... for param_name in self._INIT_PARAM_NAMES: # If this parameter was *NOT* explicitly passed by the caller, # default this parameter to its current value from this object. if param_name not in kwargs: kwargs[param_name] = getattr(self, param_name) # Return a new instance of this class initialized with these arguments. return CauseSleuth(**kwargs)
def get_cause_or_none(self) -> 'Optional[str]': ''' Human-readable string describing the failure of this pith to satisfy this PEP-compliant type hint if this pith fails to satisfy this pith *or* ``None`` otherwise (i.e., if this pith satisfies this hint). Design ---------- This getter is intentionally generalized to support objects both satisfying and *not* satisfying hints as equally valid use cases. While the parent :func:`.peperror.raise_pep_call_exception` function calling this getter is *always* passed an object *not* satisfying the passed hint, this getter is under no such constraints. Why? Because this getter is also called to find which of an arbitrary number of objects transitively nested in the object passed to :func:`.peperror.raise_pep_call_exception` fails to satisfy the corresponding hint transitively nested in the hint passed to that function. For example, consider the PEP-compliant type hint ``List[Union[int, str]]`` describing a list whose items are either integers or strings and the list ``list(range(256)) + [False,]`` consisting of the integers 0 through 255 followed by boolean ``False``. Since this list is a standard sequence, the :func:`._peperrorsequence.get_cause_or_none_sequence_standard` function must decide the cause of this list's failure to comply with this hint by finding the list item that is neither an integer nor a string, implemented by by iteratively passing each list item to the :func:`._peperrorunion.get_cause_or_none_union` function. Since the first 256 items of this list are integers satisfying this hint, :func:`._peperrorunion.get_cause_or_none_union` returns ``None`` to :func:`._peperrorsequence.get_cause_or_none_sequence_standard` before finally finding the non-compliant boolean item and returning the human-readable cause. Returns ---------- Optional[str] Either: * If this object fails to satisfy this hint, human-readable string describing the failure of this object to do so. * Else, ``None``. Raises ---------- _BeartypeCallHintPepRaiseException If this type hint is either: * PEP-noncompliant (e.g., tuple union). * PEP-compliant but no getter function has been implemented to handle this category of PEP-compliant type hint yet. ''' # Getter function returning the desired string. get_cause_or_none = None # If this hint is ignorable, all possible objects satisfy this hint, # implying this hint *CANNOT* by definition be the cause of this # failure. In this case, immediately report None. if is_hint_ignorable(self.hint): return None # Else, this hint is unignorable. # # If *NO* sign uniquely identifies this hint, this hint is # PEP-noncompliant. In this case... elif self.hint_sign is None: # Avoid circular import dependencies. from beartype._decor._code._pep._error._peperrortype import ( get_cause_or_none_type) # Defer to the getter function supporting non-"typing" classes. get_cause_or_none = get_cause_or_none_type # Else, this hint is PEP-compliant. # # If this PEP-compliant hint is its own unsubscripted "typing" # attribute (e.g., "typing.List" rather than "typing.List[str]") and is # thus subscripted by *NO* child hints... elif self.hint is self.hint_sign: # If this hint is the non-standard "typing.NoReturn" type hint # specific to return values... if self.hint is NoReturn: # Avoid circular import dependencies. from beartype._decor._code._pep._error._peperrorreturn import ( get_cause_or_none_noreturn) # Defer to the getter function specific to this hint. get_cause_or_none = get_cause_or_none_noreturn # Else, this hint is a standard PEP-compliant type hint supported # by both parameters and return values. In this case, we assume # this hint to originate from an origin type. else: # Avoid circular import dependencies. from beartype._decor._code._pep._error._peperrortype import ( get_cause_or_none_type_origin) # Defer to the getter function supporting hints originating # from origin types. get_cause_or_none = get_cause_or_none_type_origin # Else, this PEP-compliant hint is *NOT* its own unsubscripted "typing" # attribute. In this case... else: # If this hint is neither... if not ( # Subscripted by no child hints *NOR*... self.hint_childs or # An empty fixed-length tuple hint, whose PEP 585 (but *NOT* # PEP 484)-compliant implementation is subscripted by no child # hints *NOR*... is_hint_pep_tuple_empty(self.hint) or # A forward reference nor type variable, whose designs reside # well outside the standard "typing" dunder variable API and # are thus *NEVER* subscripted by child hints... is_hint_forwardref(self.hint) or is_hint_pep_typevar(self.hint)): # Then this hint should have been subscripted by one or more child # hints but wasn't. In this case, raise an exception. raise _BeartypeCallHintPepRaiseException( f'{self.exception_label} PEP type hint ' f'{repr(self.hint)} unsubscripted.') # Else, thus subscripted by one or more child hints (e.g., # "typing.List[str]" rather than "typing.List") # # Else, this hint is subscripted by one or more child hints. # Avoid circular import dependencies. from beartype._decor._code._pep._error.peperror import ( PEP_HINT_SIGN_TO_GET_CAUSE_FUNC) # Getter function returning the desired string for this attribute # if any *OR* "None" otherwise. get_cause_or_none = PEP_HINT_SIGN_TO_GET_CAUSE_FUNC.get( self.hint_sign, None) # If no such function has been implemented to handle this attribute # yet, raise an exception. if get_cause_or_none is None: raise _BeartypeCallHintPepRaiseException( f'{self.exception_label} PEP type hint ' f'{repr(self.hint)} unsupported (i.e., no ' f'"get_cause_or_none_"-prefixed getter function defined ' f'for this category of hint).') # Else, a getter function has been implemented to handle this # attribute. # Call this getter function with ourselves and return the string # returned by this getter. return get_cause_or_none(self)
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 raise_pep_call_exception( func: 'CallableTypes', pith_name: str, pith_value: object, ) -> None: ''' Raise a human-readable exception detailing the failure of the parameter with the passed name *or* return value if this name is the magic string ``return`` of the passed decorated function fails to satisfy the PEP-compliant type hint annotated on this parameter or return value. Design ---------- The :mod:`beartype` package actually implements two parallel PEP-compliant runtime type-checkers, each complementing the other by providing functionality unsuited for the other. These are: * The :mod:`beartype._decor._code._pep` submodule, dynamically generating optimized PEP-compliant runtime type-checking code embedded in the body of the wrapper function wrapping the decorated callable. For both efficiency and maintainability, that code only tests whether or not a parameter passed to that callable or value returned from that callable satisfies a PEP-compliant annotation on that callable; that code does *not* raise human-readable exceptions in the event that value fails to satisfy that annotation. Instead, that code defers to... * This function, performing unoptimized PEP-compliant runtime type-checking generically applicable to all wrapper functions. The aforementioned code calls this function only in the event that value fails to satisfy that annotation, in which case this function then raises a human-readable exception after discovering the underlying cause of this type failure by recursively traversing that value and annotation. While efficiency is the foremost focus of this package, efficiency is irrelevant during exception handling -- which typically only occurs under infrequent edge cases. Likewise, while raising this exception *would* technically be feasible from the aforementioned code, doing so proved sufficiently non-trivial, fragile, and ultimately unmaintainable to warrant offloading to this function universally callable from all wrapper functions. Parameters ---------- func : CallableTypes Decorated callable to raise this exception from. pith_name : str Either: * If the object failing to satisfy this hint is a passed parameter, the name of this parameter. * Else, the magic string ``return`` implying this object to be the value returned from this callable. pith_value : object Passed parameter or returned value failing to satisfy this hint. Raises ---------- BeartypeCallHintPepParamException If the object failing to satisfy this hint is a parameter. BeartypeCallHintPepReturnException If the object failing to satisfy this hint is a return value. BeartypeDecorHintPepException If the type hint annotating this object is *not* PEP-compliant. _BeartypeCallHintPepRaiseException If the parameter or return value with the passed name is unannotated. _BeartypeCallHintPepRaiseDesynchronizationException If this pith actually satisfies this hint, implying either: * The parent wrapper function generated by the :mod:`beartype.beartype` decorator type-checking this pith triggered a false negative by erroneously misdetecting this pith as failing this type check. * This child helper function re-type-checking this pith triggered a false positive by erroneously misdetecting this pith as satisfying this type check when in fact this pith fails to do so. ''' assert callable(func), f'{repr(func)} uncallable.' assert isinstance(pith_name, str), f'{repr(pith_name)} not string.' # print('''raise_pep_call_exception( # func={!r}, # pith_name={!r}, # pith_value={!r}', # )'''.format(func, pith_name, pith_value)) # Type of exception to be raised. exception_cls = None # Human-readable label describing this parameter or return value. pith_label = None # If the name of this parameter is the magic string implying the passed # object to be a return value, set the above local variables appropriately. if pith_name == 'return': exception_cls = BeartypeCallHintPepReturnException pith_label = label_callable_decorated_return_value( func=func, return_value=pith_value) # Else, the passed object is a parameter. In this case, set the above local # variables appropriately. else: exception_cls = BeartypeCallHintPepParamException pith_label = label_callable_decorated_param_value( func=func, param_name =pith_name, param_value=pith_value, ) # If this parameter or return value is unannotated, raise an exception. # # Note that this should *NEVER* occur, as the caller guarantees this # parameter or return value to be annotated. Nonetheless, since callers # could deface the "__annotations__" dunder dictionary without our # knowledge or permission, precautions are warranted. if pith_name not in func.__annotations__: raise _BeartypeCallHintPepRaiseException(f'{pith_label} unannotated.') # Else, this parameter or return value is annotated. # PEP-compliant type hint annotating this parameter or return value. hint = func.__annotations__[pith_name] # If type hint is *NOT* a supported type hint, raise an exception. die_unless_hint(hint=hint, hint_label=f'{pith_label} type hint') # Else, this type hint is supported. # Human-readable string describing the failure of this pith to satisfy this # hint if this pith fails to satisfy this hint *OR* "None" otherwise (i.e., # if this pith satisfies this hint). exception_cause = CauseSleuth( func=func, pith=pith_value, hint=hint, cause_indent='', exception_label=pith_label, ).get_cause_or_none() # If this pith does *NOT* satisfy this hint... if exception_cause: # This failure suffixed by a period if *NOT* yet suffixed by a period. exception_cause_suffixed = suffix_unless_suffixed( text=exception_cause, suffix='.') # Raise an exception of the desired class embedding this cause. raise exception_cls( f'{pith_label} violates type hint ' f'{repr(hint)}, as {exception_cause_suffixed}' ) # Else, this pith satisfies this hint. In this (hopefully uncommon) edge # case, *SOMETHING HAS GONE TERRIBLY AWRY.* In theory, this should never # happen, as the parent wrapper function performing type checking should # *ONLY* call this child helper function when this pith does *NOT* satisfy # this hint. In this case, raise an exception encouraging the end user to # submit an upstream issue with us. pith_value_repr = get_object_representation( obj=pith_value, max_len=_CAUSE_TRIM_OBJECT_REPR_MAX_LEN) raise _BeartypeCallHintPepRaiseDesynchronizationException( f'{pith_label} violates type hint {repr(hint)}, ' f'but utility function raise_pep_call_exception() ' f'suggests this object satisfies this hint. ' f'Please report this desynchronization failure to ' f'the beartype issue tracker ({URL_ISSUES}) with ' f"this object's representation and " f'accompanying exception traceback:\n{pith_value_repr}' )