Skip to content

Commit

Permalink
Granular PEP 526 messages x 4.
Browse files Browse the repository at this point in the history
This commit is the last in a commit chain improving the granularity of
both exception and warning messages emitted for **PEP 526-compliant
annotated variable assignments** (e.g., `muh_var: int | str = True`).
Previously, @beartype provided *no* contextual metadata describing these
assignments in these messages. @beartype now prefixes these messages
with substrings describing the fully-qualified class, callable, and/or
module directly performing these assignments. As example:

* Global annotated variable assignments like this:

  ```python
  bad_global: str = 0xFEEDFACE
  ```

  ...now yield violations like this:

  ```
  beartype.roar.BeartypeDoorHintViolation: Global variable
  "__main__.bad_global" value 4277009102 violates type hint <class
  'str'>, as int 4277009102 not instance of str.
  ```

* Local annotated variable assignments like this:

  ```python
  def bad_func():
      bad_local: int = "This local is bad. It's bad. It knows it."

  bad_func()
  ```

  ...now yield violations like this:

  ```
  beartype.roar.BeartypeDoorHintViolation: Callable
  paga.__main__.bad_func() local variable "bad_local" value "This local
  is bad. It's bad. It knows it." violates type hint <class 'int'>, as
  str "This local is bad. It's bad. It knows it." not instance of int.
  ```

(*Forceful effluvia loves vials of ale!*)
  • Loading branch information
leycec committed Mar 6, 2024
1 parent e2ff974 commit b9c1ddc
Show file tree
Hide file tree
Showing 7 changed files with 200 additions and 64 deletions.
62 changes: 50 additions & 12 deletions beartype/_check/error/_util/errorutiltext.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,36 @@

# ....................{ IMPORTS }....................
from beartype._util.text.utiltextansi import (
color_error,
color_repr,
color_arg_name,
color_pith,
color_type,
)
from beartype._util.text.utiltextlabel import label_type
from beartype._util.text.utiltextprefix import (
prefix_beartypeable,
prefix_callable_return,
)
from beartype._util.text.utiltextprefix import prefix_beartypeable
from beartype._util.text.utiltextrepr import represent_object
from collections.abc import Callable

# ....................{ LABELLERS }....................
def label_pith_value(pith: object) -> str:
'''
Human-readable label describing the passed value of the **current pith**
(i.e., arbitrary object violating the current type check) *not* suffixed by
delimiting whitespace.
Parameters
----------
pith : object
Arbitrary object violating the current type check.
Returns
-------
str
Human-readable label describing this pith value.
'''

# Create and return this label.
return f'{color_pith(represent_object(pith))}'

# ....................{ PREFIXERS }....................
def prefix_callable_arg_value(
func: Callable, arg_name: str, arg_value: object) -> str:
Expand Down Expand Up @@ -52,13 +70,12 @@ def prefix_callable_arg_value(
# Create and return this label.
return (
f'{prefix_beartypeable(obj=func, is_color=True)}parameter '
f'{color_error(arg_name)}='
f'{color_repr(represent_object(arg_value))} '
f'{color_arg_name(arg_name)}='
f'{prefix_pith_value(arg_value)}'
)


def prefix_callable_return_value(
func: Callable, return_value: object) -> str:
def prefix_callable_return_value(func: Callable, return_value: object) -> str:
'''
Human-readable label describing the passed trimmed return value of the
passed **decorated callable** (i.e., callable wrapped by the
Expand All @@ -81,9 +98,30 @@ def prefix_callable_return_value(
# Create and return this label.
return (
f'{prefix_beartypeable(obj=func, is_color=True)}return '
f'{color_repr(represent_object(return_value))} '
f'{prefix_pith_value(return_value)}'
)


def prefix_pith_value(pith: object) -> str:
'''
Human-readable label describing the passed value of the **current pith**
(i.e., arbitrary object violating the current type check) suffixed by
delimiting whitespace.
Parameters
----------
pith : object
Arbitrary object violating the current type check.
Returns
-------
str
Human-readable label describing this pith value.
'''

# Create and return this label.
return f'{label_pith_value(pith)} '

# ....................{ REPRESENTERS }....................
def represent_pith(pith: object) -> str:
'''
Expand All @@ -105,5 +143,5 @@ def represent_pith(pith: object) -> str:
# Create and return this representation.
return (
f'{color_type(label_type(type(pith)))} '
f'{color_repr(represent_object(pith))}'
f'{label_pith_value(pith)}'
)
3 changes: 2 additions & 1 deletion beartype/_check/error/errorget.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
from beartype._check.error._util.errorutiltext import (
prefix_callable_arg_value,
prefix_callable_return_value,
prefix_pith_value,
)
from beartype._util.text.utiltextansi import color_hint
from beartype._util.text.utiltextmunge import (
Expand Down Expand Up @@ -365,7 +366,7 @@ class variable or method annotated by this hint *or* :data:`None`).
exception_cls = conf.violation_door_type

# Suffix this exception prefix with an additional noun for disambiguity.
exception_prefix = f'{exception_prefix}value '
exception_prefix = f'{exception_prefix}value {prefix_pith_value(obj)}'
# Else, the caller passed a parameter name. In this case...
else:
# If the caller also passed an exception prefix, raise an exception.
Expand Down
103 changes: 71 additions & 32 deletions beartype/_util/text/utiltextansi.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,41 @@
'''

# ....................{ CONSTANTS ~ color }....................
COLOR_GREEN = '\033[92m'
COLOR_BLUE = '\033[34m'
'''
ANSI escape sequence colouring all subsequent characters as green.
ANSI escape sequence colouring all subsequent characters as blue.
'''


COLOR_RED = '\033[31m'
COLOR_CYAN = '\033[36m'
'''
ANSI escape sequence colouring all subsequent characters as red.
ANSI escape sequence colouring all subsequent characters as **cyan** (i.e.,
light blue).
'''


COLOR_BLUE = '\033[34m'
COLOR_GREEN = '\033[32m'
'''
ANSI escape sequence colouring all subsequent characters as blue.
ANSI escape sequence colouring all subsequent characters as **green**.
'''


COLOR_MAGENTA = '\033[35m'
'''
ANSI escape sequence colouring all subsequent characters as **magenta** (i.e.,
purple, dark blue).
'''


COLOR_RED = '\033[31m'
'''
ANSI escape sequence colouring all subsequent characters as **red**.
'''


COLOR_YELLOW = '\033[33m'
'''
ANSI escape sequence colouring all subsequent characters as yellow.
ANSI escape sequence colouring all subsequent characters as **yellow**.
'''

# ....................{ CONSTANTS ~ style }....................
Expand Down Expand Up @@ -73,85 +87,110 @@ def is_str_ansi(text: str) -> bool:
return _ANSI_REGEX.search(text) is not None

# ....................{ COLOURIZERS }....................
def color_error(text: str) -> str:
def color_hint(text: str) -> str:
'''
Colour the passed substring as an error.
Colour the passed substring as a type hint.
Parameters
----------
text : str
Text to be coloured as an error.
Text to be coloured as a type hint.
Returns
-------
str
This text coloured as an error.
This text coloured as a type hint.
'''
assert isinstance(text, str), f'{repr(text)} not string.'

# Infinite one-liner. Infinite possibility.
return f'{STYLE_BOLD}{COLOR_RED}{text}{ANSI_RESET}'
# To boldly go where no one-liner has gone before.
return f'{STYLE_BOLD}{COLOR_GREEN}{text}{ANSI_RESET}'


def color_hint(text: str) -> str:
def color_pith(text: str) -> str:
'''
Colour the passed substring as a PEP-compliant type hint.
Colour the passed substring as a **pith representation** (i.e.,
machine-readable string describing the value of the object currently being
type-checked, typically created by the
:func:`beartype._util.text.utiltextrepr.represent_object` function).
Parameters
----------
text : str
Text to be coloured as a type hint.
Text to be coloured as a representation.
Returns
-------
str
This text coloured as a type hint.
This text coloured as a representation.
'''
assert isinstance(text, str), f'{repr(text)} not string.'

# To boldly go where no one-liner has gone before.
return f'{STYLE_BOLD}{COLOR_BLUE}{text}{ANSI_RESET}'
# Victory fanfare! One-liner enters the chat.
return f'{STYLE_BOLD}{COLOR_RED}{text}{ANSI_RESET}'


def color_repr(text: str) -> str:
def color_type(text: str) -> str:
'''
Colour the passed substring as a **representation** (i.e., machine-readable
string returned by the :func:`repr` builtin).
Colour the passed substring as a simple class.
Parameters
----------
text : str
Text to be coloured as a representation.
Text to be coloured as a simple class.
Returns
-------
str
This text coloured as a representation.
This text coloured as a simple class.
'''
assert isinstance(text, str), f'{repr(text)} not string.'

# Victory fanfare! One-liner enters the chat.
return f'{COLOR_YELLOW}{text}{ANSI_RESET}'
# One-liner gonna one-liner. Ya, it's been a long night.
return f'{STYLE_BOLD}{COLOR_YELLOW}{text}{ANSI_RESET}'

# ....................{ COLOURIZERS ~ name }....................
def color_attr_name(text: str) -> str:
'''
Colour the passed substring as a **Python identifier** (e.g., possibly
fully-qualified name of a module, class, callable, or variable name).
def color_type(text: str) -> str:
Parameters
----------
text : str
Text to be coloured as a Python identifier.
Returns
-------
str
This text coloured as a Python identifier.
'''
Colour the passed substring as a simple class.
assert isinstance(text, str), f'{repr(text)} not string.'

# Dope af one-liner: "You rock!"
return f'{STYLE_BOLD}{COLOR_MAGENTA}{text}{ANSI_RESET}'


def color_arg_name(text: str) -> str:
'''
Colour the passed substring as an **argument name** (i.e., of the parameter
of a :func:`beartype.beartype`-decorated callable currently being
type-checked).
Parameters
----------
text : str
Text to be coloured as a simple class.
Text to be coloured as an argument name.
Returns
-------
str
This text coloured as a simple class.
This text coloured as an argument name.
'''
assert isinstance(text, str), f'{repr(text)} not string.'

# One-liner gonna one-liner. Ya, it's been a long night.
return f'{STYLE_BOLD}{COLOR_GREEN}{text}{ANSI_RESET}'
# Blue one-liner needs food badly.
return f'{STYLE_BOLD}{COLOR_MAGENTA}{text}{ANSI_RESET}'

# ....................{ STRIPPERS }....................
def strip_str_ansi(text: str) -> str:
Expand Down
8 changes: 4 additions & 4 deletions beartype/_util/text/utiltextlabel.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ def label_callable(
get_func_args_flexible_len)
from beartype._util.func.utilfunccodeobj import get_func_codeobj
from beartype._util.func.utilfunctest import is_func_lambda
from beartype._util.text.utiltextansi import color_error
from beartype._util.text.utiltextansi import color_attr_name

# Substring prefixing the string to be returned, typically identifying the
# specialized type of that callable if that callable has a specialized type.
Expand Down Expand Up @@ -226,7 +226,7 @@ def label_callable(

# If colouring that callable, do so.
if is_color:
func_label = color_error(func_label)
func_label = color_attr_name(func_label)
# Else, we are *NOT* colouring that callable.

# If contextualizing that callable, just do it already. Go, @beartype! Go!
Expand Down Expand Up @@ -341,7 +341,7 @@ def label_type(
from beartype._util.cls.utilclstest import is_type_builtin
from beartype._util.hint.pep.proposal.utilpep544 import (
is_hint_pep544_protocol)
from beartype._util.text.utiltextansi import color_error
from beartype._util.text.utiltextansi import color_attr_name

# Label to be returned, initialized to this class' fully-qualified name.
classname = get_object_type_name(cls)
Expand Down Expand Up @@ -391,7 +391,7 @@ def label_type(

# If colouring this class, do so.
if is_color:
classname = color_error(classname)
classname = color_attr_name(classname)
# Else, we are *NOT* colouring this class.

# Return this labelled classname.
Expand Down
9 changes: 4 additions & 5 deletions beartype/claw/_ast/pep/clawastpep526.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
make_node_str,
)
from beartype._util.py.utilpyversion import IS_PYTHON_AT_LEAST_3_9
from beartype._util.text.utiltextansi import color_error
from beartype._util.text.utiltextansi import color_attr_name

# ....................{ SUBCLASSES }....................
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
Expand Down Expand Up @@ -317,8 +317,7 @@ def visit_AnnAssign(self, node: AnnAssign) -> NodeVisitResult:
var_name = f'{self._module_name_beartype}.{var_basename}' # type: ignore[attr-defined]

# Human-readable label prefixing this exception message.
exception_prefix = (
f'Global variable "{color_error(var_name)}" ')
exception_prefix = f'Global variable "{color_attr_name(var_name)}" '
# Else, the lexical scope of this parent node is *NOT* module scope.
# However, by above, this scope is also *NOT* class scope. By
# elimination, this scope *MUST* thus be a callable scope. In this
Expand All @@ -329,8 +328,8 @@ def visit_AnnAssign(self, node: AnnAssign) -> NodeVisitResult:

# Human-readable label prefixing this exception message.
exception_prefix = (
f'Callable {color_error(callable_name)} '
f'local variable "{color_error(var_basename)}" '
f'Callable {color_attr_name(callable_name)} '
f'local variable "{color_attr_name(var_basename)}" '
)
# print(f'PEP 526 exception_prefix: {exception_prefix}')

Expand Down
18 changes: 13 additions & 5 deletions beartype_test/a00_unit/a60_check/a90_door/test_checkdoor.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ def test_door_die_if_unbearable(iter_hints_piths_meta) -> None:
BeartypeDecorHintNonpepException,
BeartypeDoorHintViolation,
)
from beartype._util.text.utiltextrepr import represent_object
from beartype_test.a00_unit.data.hint.util.data_hintmetacls import (
HintPithUnsatisfiedMetadata)
from pytest import raises
Expand All @@ -172,12 +173,19 @@ def test_door_die_if_unbearable(iter_hints_piths_meta) -> None:
die_if_unbearable(pith, hint, conf=conf)

# Exception message raised by this wrapper function.
exception_str = str(exception_info.value)
exception_message = str(exception_info.value)

# Assert this raiser successfully replaced the irrelevant substring
# previously prefixing this message.
assert exception_str.startswith('Die_if_unbearable() value ')
assert ' violates type hint ' in exception_str
# Truncated representation of this pith.
pith_repr = represent_object(pith)

# Assert that this message contains a truncated representation of
# this pith.
assert pith_repr in exception_message

# Assert that this raiser successfully replaced the temporary
# placeholder previously prefixing this message.
assert 'die_if_unbearable() value ' in exception_message.lower()
assert ' violates type hint ' in exception_message
# Else, this raiser satisfies this hint. In this case...
else:
# Assert this validator raises *NO* exception when passed this pith
Expand Down

0 comments on commit b9c1ddc

Please sign in to comment.