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_forwardref(sleuth: CauseSleuth) -> Optional[str]: ''' Human-readable string describing the failure of the passed arbitrary object to satisfy the passed **forward reference type hint** (i.e., string whose value is the name of a user-defined class which has yet to be defined) 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 is HINT_PEP484_BASE_FORWARDREF, ( f'PEP type hint sign {repr(sleuth.hint_sign)} not forward reference.') # Fully-qualified classname referred to by this forward reference relative # to the decorated callable. hint_forwardref_classname = get_hint_forwardref_classname_relative_to_obj( obj=sleuth.func, hint=sleuth.hint) # User-defined class dynamically imported from this classname. hint_forwardref_class = import_module_attr( module_attr_name=hint_forwardref_classname, exception_label=( f'{sleuth.exception_label} forward reference type hint'), exception_cls=_BeartypeCallHintPepRaiseException, ) # Defer to the getter function handling non-"typing" classes. Neato! return get_cause_or_none_type(sleuth.permute(hint=hint_forwardref_class))
def _get_cause_or_none_sequence(sleuth: CauseSleuth) -> 'Optional[str]': ''' Human-readable string describing the failure of the passed arbitrary object to satisfy the passed **PEP-compliant possibly non-standard sequence type hint** (i.e., PEP-compliant type hint accepting one or more subscripted type hint arguments constraining *all* items of this object, which necessarily satisfies the :class:`collections.abc.Sequence` protocol with guaranteed ``O(1)`` indexation across all sequence items) 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_PEP_SIGNS_SEQUENCE_STANDARD or ( sleuth.hint_sign in HINT_PEP_SIGNS_TUPLE and len(sleuth.hint_childs) == 2 and sleuth.hint_childs[1] is Ellipsis ) ), f'{repr(sleuth.hint)} neither standard sequence nor variadic tuple.' # First child hint of this hint. All remaining child hints if any are # ignorable. Specifically, if this hint is: # * A standard sequence (e.g., "typing.List[str]"), this hint is # subscripted by only one child hint. # * A variadic tuple (e.g., "typing.Tuple[str, ...]"), this hint is # subscripted by only two child hints the latter of which is ignorable # syntactic chuff. hint_child = sleuth.hint_childs[0] # If this child hint is *NOT* ignorable... if not is_hint_ignorable(hint_child): # For each enumerated item of this pith... for pith_item_index, pith_item in enumerate(sleuth.pith): # Human-readable string describing the failure of this item to # satisfy this child hint if this item actually fails to satisfy # this child hint *or* "None" otherwise. pith_item_cause = sleuth.permute( pith=pith_item, hint=hint_child).get_cause_or_none() # If this item is the cause of this failure, return a substring # describing this failure by embedding this failure (itself # intended to be embedded in a longer string). if pith_item_cause is not None: return ( f'{sleuth.pith.__class__.__name__} item ' f'{pith_item_index} {pith_item_cause}') # Else, this item is *NOT* the cause of this failure. Silently # continue to the next. # Else, this child hint is ignorable. # Return "None", as all items of this pith are valid, implying this pith to # deeply satisfy this hint. return None
def get_cause_or_none_sequence_standard( sleuth: CauseSleuth) -> 'Optional[str]': ''' Human-readable string describing the failure of the passed arbitrary object to satisfy the passed **PEP-compliant standard sequence type hint** (i.e., PEP-compliant type hint accepting exactly one subscripted type hint argument constraining *all* items of this object, which necessarily satisfies the :class:`collections.abc.Sequence` protocol with guaranteed ``O(1)`` indexation across all sequence items) 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_PEP_SIGNS_SEQUENCE_STANDARD, ( f'{repr(sleuth.hint)} not standard sequence.') # Assert this sequence was subscripted by exactly one argument. Note that # the "typing" module should have already guaranteed this on our behalf. assert len(sleuth.hint_childs) == 1, ( f'Standard sequence {repr(sleuth.hint)} subscripted by ' f'multiple arguments.') # Non-"typing" class originating this attribute (e.g., "list" for "List"). hint_type_origin = get_hint_pep_type_origin(sleuth.hint) # If this pith is *NOT* an instance of this class, defer to the getter # function handling non-"typing" classes. if not isinstance(sleuth.pith, hint_type_origin): return get_cause_or_none_type(sleuth.permute(hint=hint_type_origin)) # Else, this pith is an instance of this class and is thus a sequence. # Defer to the getter function supporting simple sequences. return _get_cause_or_none_sequence(sleuth)
def get_cause_or_none_generic(sleuth: CauseSleuth) -> 'Optional[str]': ''' Human-readable string describing the failure of the passed arbitrary object to satisfy the passed `PEP 484`_-compliant **generic** (i.e., type hint subclassing a combination of one or more of the :mod:`typing.Generic` superclass, the :mod:`typing.Protocol` superclass, and/or other :mod:`typing` non-class pseudo-superclasses) 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 isinstance(sleuth.hint, type), f'{repr(sleuth.hint)} not class.' assert sleuth.hint_sign is Generic, ( f'{repr(sleuth.hint_sign)} not generic.') # If this pith is *NOT* an instance of this generic, defer to the getter # function handling non-"typing" classes. if not isinstance(sleuth.pith, sleuth.hint): return get_cause_or_none_type(sleuth) # Else, this pith is an instance of this generic. # For each pseudo-superclass of this generic... for hint_base in sleuth.hint_childs: # If this pseudo-superclass is an actual superclass, this # pseudo-superclass is effectively ignorable. Why? Because the # isinstance() call above already type-checked this pith against the # generic subclassing this superclass and thus this superclass as well. # In this case, skip to the next pseudo-superclass. if isinstance(hint_base, type): continue # Else, this pseudo-superclass is *NOT* an actual class. # # If this pseudo-superclass is neither a PEP 585-compliant type hint # *NOR* a PEP-compliant type hint defined by the "typing" module, # reduce this pseudo-superclass to a real superclass originating this # pseudo-superclass. See commentary in the "_pephint" submodule. elif not (is_hint_pep585(hint_base) and is_hint_pep_typing(hint_base)): hint_base = get_hint_pep484_generic_base_erased_from_unerased( hint_base) # Else, this pseudo-superclass is defined by the "typing" module. # If this superclass is ignorable, do so. if is_hint_ignorable(hint_base): continue # Else, this superclass is unignorable. # Human-readable string describing the failure of this pith to satisfy # this pseudo-superclass if this pith actually fails to satisfy # this pseudo-superclass *or* "None" otherwise. # print(f'tuple pith: {pith_item}\ntuple hint child: {hint_child}') pith_base_cause = sleuth.permute(hint=hint_base).get_cause_or_none() # If this pseudo-superclass is the cause of this failure, return a # substring describing this failure by embedding this failure (itself # intended to be embedded in a longer string). if pith_base_cause is not None: # print(f'tuple pith: {sleuth_copy.pith}\ntuple hint child: {sleuth_copy.hint}\ncause: {pith_item_cause}') return f'generic base {repr(hint_base)} {pith_base_cause}' # Else, this pseudo-superclass is *NOT* the cause of this failure. # Silently continue to the next. # Return "None", as this pith satisfies both this generic itself *AND* all # pseudo-superclasses subclassed by this generic, implying this pith to # deeply satisfy this hint. return None
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 get_cause_or_none_tuple(sleuth: CauseSleuth) -> 'Optional[str]': ''' Human-readable string describing the failure of the passed arbitrary object to satisfy the passed **PEP-compliant standard sequence type hint** (i.e., PEP-compliant type hint accepting exactly one subscripted type hint argument constraining *all* items of this object, which necessarily satisfies the :class:`collections.abc.Sequence` protocol with guaranteed ``O(1)`` indexation across all sequence items) 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_PEP_SIGNS_TUPLE, ( f'{repr(sleuth.hint_sign)} not tuple.') # If this pith is *NOT* an instance of this class, defer to the getter # function handling non-"typing" classes. if not isinstance(sleuth.pith, tuple): return get_cause_or_none_type(sleuth.permute(hint=tuple)) # If this hint is... if ( # This tuple is subscripted by exactly two child hints *AND*... len(sleuth.hint_childs) == 2 and # The second child hint is just an unquoted ellipsis... sleuth.hint_childs[1] is Ellipsis ): # Then this hint is of the variadic form "Tuple[{typename}, ...]", typing a # tuple accepting a variadic number of items all satisfying the # child hint "{typename}". Since this case semantically reduces to a simple # sequence, defer to the getter function supporting simple sequences. return _get_cause_or_none_sequence(sleuth) # Else, this hint is of the fixed-length form "Tuple[{typename1}, ..., # {typenameN}]", typing a tuple accepting a fixed number of items each # satisfying a unique child hint. # # If this hint is the empty fixed-length tuple, validate this pith to be # the empty tuple. elif is_hint_pep_tuple_empty(sleuth.hint): # If this pith is non-empty and thus fails to satisfy this hint... if sleuth.pith: # Truncated representation of this tuple. pith_repr = get_object_representation(sleuth.pith) # Return a substring describing this failure. return f'tuple {pith_repr} non-empty' # Else, this pith is the empty tuple and thus satisfies this hint. # Else, this hint is a standard fixed-length tuple. In this case... else: # If this pith and hint are of differing lengths, this tuple fails to # satisfy this hint. In this case... if len(sleuth.pith) != len(sleuth.hint_childs): # Truncated representation of this tuple. pith_repr = get_object_representation(sleuth.pith) # Return a substring describing this failure. return ( f'tuple {pith_repr} length ' f'{len(sleuth.pith)} not {len(sleuth.hint_childs)}' ) # Else, this pith and hint are of the same length. # For each enumerated item of this tuple... for pith_item_index, pith_item in enumerate(sleuth.pith): # Child hint corresponding to this tuple item. Since this pith and # hint are of the same length, this child hint exists. hint_child = sleuth.hint_childs[pith_item_index] # If this child hint is ignorable, continue to the next. if is_hint_ignorable(hint_child): continue # Else, this child hint is unignorable. # Human-readable string describing the failure of this tuple item # to satisfy this child hint if this item actually fails to satisfy # this child hint *or* "None" otherwise. # print(f'tuple pith: {pith_item}\ntuple hint child: {hint_child}') # sleuth_copy = sleuth.permute(pith=pith_item, hint=hint_child) # pith_item_cause = sleuth_copy.get_cause_or_none() pith_item_cause = sleuth.permute( pith=pith_item, hint=hint_child).get_cause_or_none() # If this item is the cause of this failure, return a substring # describing this failure by embedding this failure (itself # intended to be embedded in a longer string). if pith_item_cause is not None: # print(f'tuple pith: {sleuth_copy.pith}\ntuple hint child: {sleuth_copy.hint}\ncause: {pith_item_cause}') return f'tuple item {pith_item_index} {pith_item_cause}' # Else, this item is *NOT* the cause of this failure. Silently # continue to the next. # Return "None", as all items of this fixed-length tuple are valid, # implying this pith to deeply satisfy this hint. return None
def _get_cause_or_none_sequence(sleuth: CauseSleuth) -> Optional[str]: ''' Human-readable string describing the failure of the passed arbitrary object to satisfy the passed **PEP-compliant variadic sequence type hint** (i.e., PEP-compliant type hint accepting one or more subscripted type hint arguments constraining *all* items of this object, which necessarily satisfies the :class:`collections.abc.Sequence` protocol with guaranteed ``O(1)`` indexation across all sequence items) 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 this type hint to describe a variadic sequence. See the parent # get_cause_or_none_sequence_standard() and get_cause_or_none_tuple() # functions for derivative logic. # # Note that this pith need *NOT* be validated to be an instance of the # expected variadic sequence, as the caller guarantees this to be the case. assert isinstance(sleuth, CauseSleuth), f'{repr(sleuth)} not cause sleuth.' assert (sleuth.hint_sign in HINT_PEP_SIGNS_SEQUENCE_STANDARD or (sleuth.hint_sign in HINT_PEP_SIGNS_TUPLE and len(sleuth.hint_childs) == 2 and sleuth.hint_childs[1] is Ellipsis)), ( f'{repr(sleuth.hint)} neither ' f'standard sequence nor variadic tuple hint.') # If this sequence is non-empty... if sleuth.pith: # First child hint of this hint. All remaining child hints if any are # ignorable. Specifically, if this hint is: # * A standard sequence (e.g., "typing.List[str]"), this hint is # subscripted by only one child hint. # * A variadic tuple (e.g., "typing.Tuple[str, ...]"), this hint is # subscripted by only two child hints the latter of which is # ignorable syntactic chuff. hint_child = sleuth.hint_childs[0] # If this child hint is *NOT* ignorable... if not is_hint_ignorable(hint_child): # Arbitrary iterator satisfying the enumerate() protocol, yielding # zero or more 2-tuples of the form "(item_index, item)", where: # * "item" is an arbitrary item of this sequence. # * "item_index" is the 0-based index of this item. pith_enumerator = None # If this sequence was indexed by the parent @beartype-generated # wrapper function by a pseudo-random integer in O(1) time, # type-check *ONLY* the same index of this sequence also in O(1) # time. Since the current call to that function failed a # type-check, either this index is the index responsible for that # failure *OR* this sequence is valid and another container # entirely is responsible for that failure. In either case, no # other indices of this sequence need be checked. if sleuth.random_int is not None: # 0-based index of this item calculated from this random # integer in the *SAME EXACT WAY* as in the parent # @beartype-generated wrapper function. pith_item_index = sleuth.random_int % len(sleuth.pith) # Pseudo-random item with this index in this sequence. pith_item = sleuth.pith[pith_item_index] # 2-tuple of this index and item in the same order as the # 2-tuples returned by the enumerate() builtin. pith_enumeratable = (pith_item_index, pith_item) # Iterator yielding only this 2-tuple. pith_enumerator = iter((pith_enumeratable, )) # print(f'Checking item {pith_item_index} in O(1) time!') # Else, this sequence was iterated by the parent # @beartype-generated wrapper function in O(n) time. In this case, # type-check *ALL* indices of this sequence in O(n) time as well. else: # Iterator yielding all indices and items of this sequence. pith_enumerator = enumerate(sleuth.pith) # print('Checking sequence in O(n) time!') # For each enumerated item of this (sub)sequence... for pith_item_index, pith_item in pith_enumerator: # Human-readable string describing the failure of this item # to satisfy this child hint if this item actually fails to # satisfy this child hint *or* "None" otherwise. pith_item_cause = sleuth.permute( pith=pith_item, hint=hint_child).get_cause_or_none() # If this item is the cause of this failure, return a # substring describing this failure by embedding this # failure (itself intended to be embedded in a longer # string). if pith_item_cause is not None: return (f'{sleuth.pith.__class__.__name__} item ' f'{pith_item_index} {pith_item_cause}') # Else, this item is *NOT* the cause of this failure. # Silently continue to the next. # Else, this child hint is ignorable. # Else, this sequence is empty, in which case all items of this sequence # (of which there are none) are valid. Just go with it, people. # Return "None", as all items of this sequence are valid, implying this # sequence to deeply satisfy this hint. return None