From 2235560c0d563d2c2002d06e2dc089e00eca322d Mon Sep 17 00:00:00 2001 From: leycec Date: Sat, 6 May 2023 02:47:19 -0400 Subject: [PATCH] `beartype.claw` resurrection x 7. This commit is the next in a commit chain resurrecting our *nearly* complete `beartype.claw` API from an early and ignominious grave, en-route to resolving feature request #43 kindly submitted a literal lifetime ago by @ZeevoX Beeblebrox the gentle typing giant. Specifically, this commit does... such a glut of new things that @leycec rapidly lost the plot as to what, exactly, if anything, he did. It's likely that this is important stuff, however. Let's believe it. (*Impressive imps limp regressively!*) --- beartype/_decor/decorcore.py | 35 +++++----- beartype/_util/cls/utilclsget.py | 60 ++++++++++++++++- beartype/_util/func/utilfuncfile.py | 14 ++-- beartype/_util/mod/utilmodget.py | 54 +++++++-------- beartype/_util/mod/utilmodimport.py | 43 ++++++++++-- beartype/_util/text/utiltextansi.py | 13 ++-- beartype/_util/text/utiltextlabel.py | 67 +++++++++++++------ beartype/_util/utilobject.py | 66 +++++++++++++++--- .../a00_unit/a20_util/cls/test_utilclsget.py | 44 ++++++++++++ .../a00_unit/a20_util/cls/test_utilclstest.py | 2 +- .../a20_util/func/test_utilfuncfile.py | 24 ++++--- .../a00_unit/a20_util/test_utilobject.py | 27 +++++++- .../a20_util/text/test_utiltextansi.py | 52 ++++++++++++++ beartype_test/a00_unit/data/data_type.py | 47 +++++++++++-- .../data/util/func/data_utilfunccode.py | 4 +- doc/src/_links.rst | 2 + doc/src/faq.rst | 25 ++++--- doc/src/pep.rst | 2 +- 18 files changed, 455 insertions(+), 126 deletions(-) create mode 100644 beartype_test/a00_unit/a20_util/cls/test_utilclsget.py create mode 100644 beartype_test/a00_unit/a20_util/text/test_utiltextansi.py diff --git a/beartype/_decor/decorcore.py b/beartype/_decor/decorcore.py index 4ac81afb..14407b3b 100644 --- a/beartype/_decor/decorcore.py +++ b/beartype/_decor/decorcore.py @@ -206,7 +206,6 @@ def beartype_object_nonfatal( warning_category: TypeWarning, # Optional parameters. - cls_stack: TypeStack = None, **kwargs ) -> BeartypeableT: ''' @@ -244,11 +243,6 @@ def beartype_object_nonfatal( warning_category : TypeWarning Category of the non-fatal warning to emit if :func:`beartype.beartype` fails to generate a type-checking wrapper for this callable or class. - cls_stack : TypeStack, optional - **Type stack** (i.e., either a tuple of the one or more - :func:`beartype.beartype`-decorated classes lexically containing the - class variable or method annotated by this hint *or* :data:`None`). - Defaults to :data:`None`. All remaining keyword parameters are passed as is to the lower-level :func:`.beartype_object` decorator internally called by this higher-level @@ -284,16 +278,19 @@ class variable or method annotated by this hint *or* :data:`None`). assert isinstance(warning_category, Warning), ( f'{repr(warning_category)} not warning category.') - #FIXME: Unconditionally munge this error message by: - #* Globally replacing *EACH* newline (i.e., "\n" substring) in this - # message with a newline followed by four spaces (i.e., "\n "). - #* Stripping all ANSI colors. While colors are useful for exception - # messages that typically percolate down to the terminal, warnings are - # another breed entirely. Maybe? Maybe. + # Avoid circular import dependencies. + from beartype._util.text.utiltextansi import strip_text_ansi + from beartype._util.text.utiltextlabel import label_beartypeable_kind + from beartype._util.text.utiltextmunge import uppercase_char_first # Original error message to be embedded in the warning message to be - # emitted, defined as either... - error_message = ( + # emitted, stripped of *ALL* ANSI color. While colors improve the + # readability of exception messages that percolate down to an ANSI-aware + # command line, warnings are usually harvested and then regurgitated by + # intermediary packages into ANSI-unaware logfiles. + # + # This message is defined as either... + error_message = strip_text_ansi( # If this exception is beartype-specific, this exception's message # is probably human-readable as is. In this case, coerce only that # message directly into a warning for brevity and readability. @@ -308,6 +305,12 @@ class variable or method annotated by this hint *or* :data:`None`). format_exc() ) + # Globally replace *EVERY* newline in this message with a newline + # followed by four spaces. Doing so visually offsets this lower-level + # exception message from the higher-level warning message embedding this + # exception message. + error_message.replace('\n', '\n ') + #FIXME: Woops. Looks like we accidentally duplicated functionality here #that already exists in the label_callable() function. Let's generalize #that functionality out of label_callable() into a lower-level @@ -320,10 +323,6 @@ class variable or method annotated by this hint *or* :data:`None`). # # obj_label_capitalized = uppercase_char_first(obj_label) - # Avoid circular import dependencies. - from beartype._util.text.utiltextlabel import label_beartypeable_kind - from beartype._util.text.utiltextmunge import uppercase_char_first - # Fully-qualified name of this beartypeable. obj_name = get_object_name(obj) diff --git a/beartype/_util/cls/utilclsget.py b/beartype/_util/cls/utilclsget.py index 4ecdbd78..c5c8afec 100644 --- a/beartype/_util/cls/utilclsget.py +++ b/beartype/_util/cls/utilclsget.py @@ -12,12 +12,70 @@ # ....................{ IMPORTS }.................... from beartype.roar._roarexc import _BeartypeUtilTypeException +from beartype.typing import Optional from beartype._data.datatyping import ( LexicalScope, TypeException, ) -# ....................{ VALIDATORS }.................... +# ....................{ GETTERS }.................... +#FIXME: Unit test us up. +def get_type_filename_or_none(cls: type) -> Optional[str]: + ''' + Absolute filename of the file on the local filesystem containing the + pure-Python source code for the script or module defining the passed class + if that class is defined on-disk *or* :data:`None` otherwise (i.e., if that + class is dynamically defined in-memory by a prior call to the :func:`exec` + or :func:`eval` builtins). + + Parameters + ---------- + cls : type + Class to be inspected. + + Returns + ---------- + Optional[str] + Either: + + * If this class was physically declared by a file, the absolute filename + of that file. + * If this class was dynamically declared in-memory, :data:`None`. + ''' + + # Avoid circular import dependencies. + from beartype._util.mod.utilmodget import ( + get_module_filename_or_none, + get_object_module_name_or_none, + ) + from beartype._util.mod.utilmodimport import get_module_imported_or_none + + # Fully-qualified name of the module declaring this type if any *OR* "None". + # + # Note that *ALL* types should be declared by *SOME* modules. Nonetheless, + # this is Python. It's best to assume the worst. + type_module_name = get_object_module_name_or_none(cls) + + # If a module declares this type... + if type_module_name: + # This module if previously imported *OR* "None". + # + # Note that this module *SHOULD* necessarily already have been imported, + # as this type obviously exists. Nonetheless, this module will be + # unimportable for types dynamically declared in-memory rather than + # on-disk, in which case the name of this module will have been a lie. + type_module = get_module_imported_or_none(type_module_name) + + # If this module was previously imported... + if type_module: + # Return the filename defining this module if any *OR* "None". + return get_module_filename_or_none(type_module) + + # If all else fails, this type was probably declared in-memory rather than + # on-disk. In this case, fallback to merely returning "None". + return None + + #FIXME: Unit test us up, please. def get_type_locals( # Mandatory parameters. diff --git a/beartype/_util/func/utilfuncfile.py b/beartype/_util/func/utilfuncfile.py index 93437673..d0af4921 100644 --- a/beartype/_util/func/utilfuncfile.py +++ b/beartype/_util/func/utilfuncfile.py @@ -65,7 +65,7 @@ def is_func_file(func: Callable) -> bool: # One-liners for abstruse abstraction. return get_func_filename_or_none(func) is not None -# ....................{ GETTERS ~ code : lines }.................... +# ....................{ GETTERS }.................... def get_func_filename_or_none( # Mandatory parameters. func: Callable, @@ -76,9 +76,9 @@ def get_func_filename_or_none( ''' Absolute filename of the file on the local filesystem containing the pure-Python source code for the script or module defining the passed - callable if that callable is defined on-disk *or* ``None`` otherwise (i.e., - if that callable is dynamically defined in-memory by a prior call to the - :func:`exec` or :func:`eval` builtins). + callable if that callable is defined on-disk *or* :data:`None` otherwise + (i.e., if that callable is dynamically defined in-memory by a prior call to + the :func:`exec` or :func:`eval` builtins). Parameters ---------- @@ -90,9 +90,9 @@ def get_func_filename_or_none( Optional[str] Either: - * If the passed callable was physically declared by a file, the - absolute filename of that file. - * If the passed callable was dynamically declared in-memory, ``None``. + * If that callable was physically declared by a file, the absolute + filename of that file. + * If that callable was dynamically declared in-memory, :data:`None`. ''' # Code object underlying the passed callable if that callable is pure-Python diff --git a/beartype/_util/mod/utilmodget.py b/beartype/_util/mod/utilmodget.py index 5f28bd32..da9c7671 100644 --- a/beartype/_util/mod/utilmodget.py +++ b/beartype/_util/mod/utilmodget.py @@ -150,33 +150,6 @@ def get_object_module_line_number_begin(obj: object) -> int: f'{repr(obj)} neither callable nor class.') # ....................{ GETTERS ~ object : name }.................... -#FIXME: Unit test us up, please. -def get_object_module_name_or_none(obj: object) -> Optional[str]: - ''' - **Fully-qualified name** (i.e., ``.``-delimited name prefixed by the - declaring package) of the module declaring the passed object if this object - defines the ``__module__`` dunder instance variable *or* :data:`None` - otherwise. - - Parameters - ---------- - obj : object - Object to be inspected. - - Returns - ---------- - Optional[str] - Either: - - * Fully-qualified name of the module declaring this object if this - object declares a ``__module__`` dunder attribute. - * :data:`None` otherwise. - ''' - - # Let it be, speaking one-liners of wisdom. - return getattr(obj, '__module__', None) - - #FIXME: Unit test us up, please. def get_object_module_name(obj: object) -> str: ''' @@ -217,6 +190,33 @@ def get_object_module_name(obj: object) -> str: # Return this name. return module_name + +#FIXME: Unit test us up, please. +def get_object_module_name_or_none(obj: object) -> Optional[str]: + ''' + **Fully-qualified name** (i.e., ``.``-delimited name prefixed by the + declaring package) of the module declaring the passed object if this object + defines the ``__module__`` dunder instance variable *or* :data:`None` + otherwise. + + Parameters + ---------- + obj : object + Object to be inspected. + + Returns + ---------- + Optional[str] + Either: + + * Fully-qualified name of the module declaring this object if this + object declares a ``__module__`` dunder attribute. + * :data:`None` otherwise. + ''' + + # Let it be, speaking one-liners of wisdom. + return getattr(obj, '__module__', None) + # ....................{ GETTERS ~ object : type : name }.................... #FIXME: Unit test us up, please. def get_object_type_module_name_or_none(obj: object) -> Optional[str]: diff --git a/beartype/_util/mod/utilmodimport.py b/beartype/_util/mod/utilmodimport.py index af5d9873..f194c559 100644 --- a/beartype/_util/mod/utilmodimport.py +++ b/beartype/_util/mod/utilmodimport.py @@ -23,9 +23,34 @@ from types import ModuleType from warnings import warn +# ....................{ GETTERS }.................... +#FIXME: Unit test us up, please. +def get_module_imported_or_none(module_name: str) -> Optional[ModuleType]: + ''' + Previously imported module, package, or C extension with the passed + fully-qualified name if previously imported *or* :data:`None` otherwise + (i.e., if that module, package, or C extension has yet to be imported). + + Parameters + ---------- + module_name : str + Fully-qualified name of the previously imported module to be returned. + + Returns + ---------- + Either: + + * If a module, package, or C extension with this fully-qualified name has + already been imported, that module, package, or C extension. + * Else, :data:`None`. + ''' + + # Donkey One-liner Country: Codebase Freeze! + return sys_modules.get(module_name) + # ....................{ IMPORTERS }.................... #FIXME: Preserved until requisite, which shouldn't be long. -#FIXME: Unit test us up. +#FIXME: Unit test us up, please. # def import_module( # # Mandatory parameters. # module_name: str, @@ -75,7 +100,7 @@ def import_module_or_none(module_name: str) -> Optional[ModuleType]: ''' Dynamically import and return the module, package, or C extension with the - passed fully-qualified name if importable *or* return ``None`` otherwise + passed fully-qualified name if importable *or* return :data:`None` otherwise (i.e., if that module, package, or C extension is unimportable). For safety, this function also emits a non-fatal warning when that module, @@ -87,6 +112,14 @@ def import_module_or_none(module_name: str) -> Optional[ModuleType]: module_name : str Fully-qualified name of the module to be imported. + Returns + ---------- + Either: + + * If a module, package, or C extension with this fully-qualified name is + importable, that module, package, or C extension. + * Else, :data:`None`. + Warns ---------- BeartypeModuleUnimportableWarning @@ -97,7 +130,7 @@ def import_module_or_none(module_name: str) -> Optional[ModuleType]: # Module cached with "sys.modules" if this module has already been imported # elsewhere under the active Python interpreter *OR* "None" otherwise. - module = sys_modules.get(module_name) + module = get_module_imported_or_none(module_name) # If this module has already been imported, return this cached module. if module is not None: @@ -205,7 +238,7 @@ def import_module_attr_or_none( ''' Dynamically import and return the **module attribute** (i.e., object declared at module scope) with the passed fully-qualified name if - importable *or* return ``None`` otherwise. + importable *or* return :data:`None` otherwise. Parameters ---------- @@ -223,7 +256,7 @@ def import_module_attr_or_none( object Either: - * If *no* module prefixed this name exists, ``None``. + * If *no* module prefixed this name exists, :data:`None`. * If a module prefixed by this name exists *but* that module declares no attribute by this name, ``None``. * Else, the module attribute with this fully-qualified name. diff --git a/beartype/_util/text/utiltextansi.py b/beartype/_util/text/utiltextansi.py index 0bb573a9..cc9a23f6 100644 --- a/beartype/_util/text/utiltextansi.py +++ b/beartype/_util/text/utiltextansi.py @@ -50,20 +50,20 @@ ''' # ....................{ TESTERS }.................... -#FIXME: Unit test us up, please. def is_text_ansi(text: str) -> bool: ''' - ``True`` if the passed text contains one or more ANSI escape sequences. + :data:`True` if the passed text contains one or more ANSI escape sequences. Parameters ---------- text : str - Text to be tested for ANSI. + Text to be tested. Returns ---------- bool - ``True`` only if this text contains one or more ANSI escape sequences. + :data:`True` only if this text contains one or more ANSI escape + sequences. ''' assert isinstance(text, str), f'{repr(text)} not string.' @@ -72,15 +72,14 @@ def is_text_ansi(text: str) -> bool: return _ANSI_REGEX.search(text) is not None # ....................{ STRIPPERS }.................... -#FIXME: Unit test us up, please. def strip_text_ansi(text: str) -> str: ''' - Strip all ANSI escape sequences from the passed string. + Strip *all* ANSI escape sequences from the passed string. Parameters ---------- text : str - Text to be stripped of ANSI. + Text to be stripped. Returns ---------- diff --git a/beartype/_util/text/utiltextlabel.py b/beartype/_util/text/utiltextlabel.py index 2718fe5a..ad5d5985 100644 --- a/beartype/_util/text/utiltextlabel.py +++ b/beartype/_util/text/utiltextlabel.py @@ -233,31 +233,58 @@ def label_callable( func_label_prefix = 'asynchronous generator ' # Else, that callable is *NOT* an asynchronous generator. - # If contextualizing that callable... + # If contextualizing that callable, just do it already. Go, @beartype! Go! if is_context: - #FIXME: Define a comparable get_type_filename_or_none() getter whose - #implementation should probably resemble this StackOverflow answer: - # https://stackoverflow.com/a/697356/2809027 - - # Absolute filename of the source module file defining that callable if - # that callable was defined on-disk *OR* "None" otherwise (i.e., if that - # callable was defined in-memory). - func_filename = get_func_filename_or_none(func) - - # Line number of the first line declaring that callable in that file. - func_lineno = get_object_module_line_number_begin(func) - - # If that callable was defined on-disk, describe the location of that - # callable in that file. - if func_filename: - func_label_suffix += ( - f' declared on line {func_lineno} of file "{func_filename}" ') - # Else, that callable was defined in-memory. In this case, avoid - # attempting to uselessly contextualize that callable. + func_label_suffix += f' {label_object_context(func)} ' # Return that prefix followed by the fully-qualified name of that callable. return f'{func_label_prefix}{get_object_name(func)}(){func_label_suffix}' +# ....................{ LABELLERS ~ context }.................... +#FIXME: Unit test us up, please. +def label_object_context(obj: object) -> str: + ''' + Human-readable label describing the **context** (i.e., absolute filename of + the module or script physically declaring the passed object *and* the + 1-based line number of the first line declaring this object in this file) of + this object if this object is either a callable or class declared on-disk + *or* the empty string otherwise (i.e., if this object is neither a callable + nor class *or* is either a callable or class declared in-memory). + + Parameters + ---------- + func : object + Object to label the context of. + + Returns + ---------- + str + Human-readable label describing the context of this object. + ''' + + # Defer test-specific imports. + from beartype._util.utilobject import get_object_filename_or_none + from beartype._util.mod.utilmodget import ( + get_object_module_line_number_begin) + + # Absolute filename of the module or script physically declaring this object + # if this object was defined on-disk *OR* "None" otherwise (i.e., if this + # object was defined in-memory). + obj_filename = get_object_filename_or_none(obj) + + # If this object is defined on-disk... + if obj_filename: + # Line number of the first line declaring this object in that file. + obj_lineno = get_object_module_line_number_begin(obj) + + # Return a string describing the context of this object. + return f'declared on line {obj_lineno} of file "{obj_filename}"' + # Else, this object was defined in-memory. In this case, avoid attempting to + # needlessly contextualize this object. + + # Let's hear it for giving up here and going home. Yeah! Go, @beartype! + return '' + # ....................{ LABELLERS ~ exception }.................... def label_exception(exception: Exception) -> str: ''' diff --git a/beartype/_util/utilobject.py b/beartype/_util/utilobject.py index c85ed5d2..57c5b84b 100644 --- a/beartype/_util/utilobject.py +++ b/beartype/_util/utilobject.py @@ -12,8 +12,11 @@ # ....................{ IMPORTS }.................... from beartype.roar._roarexc import _BeartypeUtilObjectNameException +from beartype.typing import ( + Any, + Optional, +) from contextlib import AbstractContextManager -from beartype.typing import Any # ....................{ CLASSES }.................... class Iota(object): @@ -36,9 +39,9 @@ class Iota(object): # ....................{ TESTERS }.................... def is_object_context_manager(obj: object) -> bool: ''' - ``True`` only if the passed object is a **context manager** (i.e., object - defining both the ``__exit__`` and ``__enter__`` dunder methods required to - satisfy the context manager protocol).. + :data:`True` only if the passed object is a **context manager** (i.e., + object defining both the ``__exit__`` and ``__enter__`` dunder methods + required to satisfy the context manager protocol). Parameters ---------- @@ -48,7 +51,7 @@ def is_object_context_manager(obj: object) -> bool: Returns ---------- bool - ``True`` only if this object is a context manager. + :data:`True` only if this object is a context manager. ''' # One-liners for frivolous inanity. @@ -59,8 +62,8 @@ def is_object_context_manager(obj: object) -> bool: # decorator, which requires all passed parameters to already be hashable. def is_object_hashable(obj: object) -> bool: ''' - ``True`` only if the passed object is **hashable** (i.e., passable to the - builtin :func:`hash` function *without* raising an exception and thus + :data:`True` only if the passed object is **hashable** (i.e., passable to + the builtin :func:`hash` function *without* raising an exception and thus usable in hash-based containers like dictionaries and sets). Parameters @@ -71,7 +74,7 @@ def is_object_hashable(obj: object) -> bool: Returns ---------- bool - ``True`` only if this object is hashable. + :data:`True` only if this object is hashable. ''' # Attempt to hash this object. If doing so raises *any* exception @@ -264,6 +267,53 @@ def get_object_basename_scoped(obj: Any) -> str: # Remove all "" placeholder substrings as discussed above. return object_scoped_name.replace('.', '') +# ....................{ GETTERS ~ filename }.................... +def get_object_filename_or_none(obj: object) -> Optional[str]: + ''' + Filename of the module or script physically declaring the passed object if + this object is either a callable or class physically declared on-disk *or* + :data:`None` otherwise (i.e., if this object is neither a callable nor + class *or* is either a callable or class dynamically declared in-memory). + + Parameters + ---------- + obj : object + Object to be inspected. + + Returns + ---------- + Optional[str] + Either: + + * If this object is either a callable or class physically declared + on-disk, the filename of the module or script physically declaring + this object. + * Else, :data:`None`. + ''' + + # Avoid circular import dependencies. + from beartype._util.cls.utilclsget import get_type_filename_or_none + from beartype._util.func.utilfuncfile import get_func_filename_or_none + from beartype._util.func.utilfunctest import is_func_python + + # Return either... + return ( + # If this object is a pure-Python class, the absolute filename of the + # source module file defining that class if that class was defined + # on-disk *OR* "None" otherwise (i.e., if that class was defined + # in-memory); + get_type_filename_or_none(obj) + if isinstance(obj, type) else + # If this object is a pure-Python callable, the absolute filename of the + # absolute filename of the source module file defining that callable if + # that callable was defined on-disk *OR* "None" otherwise (i.e., if that + # callable was defined in-memory); + get_func_filename_or_none(obj) + if is_func_python(obj) else + # Else, "None". + None + ) + # ....................{ GETTERS ~ type }.................... def get_object_type_unless_type(obj: object) -> type: ''' diff --git a/beartype_test/a00_unit/a20_util/cls/test_utilclsget.py b/beartype_test/a00_unit/a20_util/cls/test_utilclsget.py new file mode 100644 index 00000000..1027a275 --- /dev/null +++ b/beartype_test/a00_unit/a20_util/cls/test_utilclsget.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# --------------------( LICENSE )-------------------- +# Copyright (c) 2014-2023 Beartype authors. +# See "LICENSE" for further details. + +''' +Project-wide **class getter** unit tests. + +This submodule unit tests the public API of the private +:mod:`beartype._util.cls.utilclsget` submodule. +''' + +# ....................{ IMPORTS }.................... +#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +# WARNING: To raise human-readable test errors, avoid importing from +# package-specific submodules at module scope. +#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +# ....................{ TESTS ~ getter }.................... +def test_get_type_filename_or_none() -> None: + ''' + Test the + :func:`beartype._util.cls.utilclsget.get_type_filename_or_none` getter. + ''' + + # Defer test-specific imports. + from beartype._util.cls.utilclsget import get_type_filename_or_none + from beartype_test.a00_unit.data.data_type import ( + Class, + ClassWithModuleNameNone, + ClassWithModuleNameNonexistent, + ) + + # Filename of a class declared on-disk. + type_filename = get_type_filename_or_none(Class) + + # Assert this filename is that of the expected submodule. + assert isinstance(type_filename, str) + assert 'data_type' in type_filename + + # Assert this getter returns "None" for classes with either missing or + # non-existent module names. + assert get_type_filename_or_none(ClassWithModuleNameNone) is None + assert get_type_filename_or_none(ClassWithModuleNameNonexistent) is None diff --git a/beartype_test/a00_unit/a20_util/cls/test_utilclstest.py b/beartype_test/a00_unit/a20_util/cls/test_utilclstest.py index cc899eeb..6866a850 100644 --- a/beartype_test/a00_unit/a20_util/cls/test_utilclstest.py +++ b/beartype_test/a00_unit/a20_util/cls/test_utilclstest.py @@ -4,7 +4,7 @@ # See "LICENSE" for further details. ''' -Project-wide **class-specific utility function** unit tests. +Project-wide **class tester** unit tests. This submodule unit tests the public API of the private :mod:`beartype._util.cls.utilclstest` submodule. diff --git a/beartype_test/a00_unit/a20_util/func/test_utilfuncfile.py b/beartype_test/a00_unit/a20_util/func/test_utilfuncfile.py index c37ccf6d..0eb8af75 100644 --- a/beartype_test/a00_unit/a20_util/func/test_utilfuncfile.py +++ b/beartype_test/a00_unit/a20_util/func/test_utilfuncfile.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# --------------------( LICENSE )-------------------- +# --------------------( LICENSE )-------------------- # Copyright (c) 2014-2023 Beartype authors. # See "LICENSE" for further details. @@ -10,17 +10,17 @@ :mod:`beartype._util.func.utilfuncfile` submodule. ''' -# ....................{ IMPORTS }.................... -#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +# ....................{ IMPORTS }.................... +#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! # WARNING: To raise human-readable test errors, avoid importing from # package-specific submodules at module scope. -#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -# ....................{ TESTS }.................... +# ....................{ TESTS ~ tester }.................... def test_is_func_file() -> None: ''' - Test usage of the - :func:`beartype._util.func.utilfuncfile.is_func_file` function. + Test the + :func:`beartype._util.func.utilfuncfile.is_func_file` tester. ''' # Defer test-specific imports. @@ -43,18 +43,19 @@ def but_now_thy_youngest_dearest_one_has_perished(): # Assert this tester rejects C-based callables. assert is_func_file(iter) is False - +# ....................{ TESTS ~ getter }.................... def test_get_func_filename_or_none() -> None: ''' - Test usage of the - :func:`beartype._util.func.utilfuncfile.get_func_filename_or_none` - function. + Test the + :func:`beartype._util.func.utilfuncfile.get_func_filename_or_none` getter. ''' + # ....................{ IMPORTS }.................... # Defer test-specific imports. from beartype._util.func.utilfuncfile import get_func_filename_or_none from beartype._util.func.utilfuncmake import make_func + # ....................{ CALLABLES }.................... # Arbitrary pure-Python callable declared on-disk. def and_this_the_naked_countenance_of_earth(): return 'On which I gaze, even these primeval mountains' @@ -75,6 +76,7 @@ def and_this_the_naked_countenance_of_earth(): # by the standard "linecache" module. teach_the_adverting_mind = eval('lambda: "The glaciers creep"') + # ....................{ ASSERTS }.................... # Assert this getter returns "None" when passed a C-based callable. assert get_func_filename_or_none(iter) is None diff --git a/beartype_test/a00_unit/a20_util/test_utilobject.py b/beartype_test/a00_unit/a20_util/test_utilobject.py index bfdbb074..d9a5cd33 100644 --- a/beartype_test/a00_unit/a20_util/test_utilobject.py +++ b/beartype_test/a00_unit/a20_util/test_utilobject.py @@ -72,10 +72,33 @@ def test_get_object_basename_scoped() -> None: 'From the ice-gulfs that gird his secret throne,') +def test_get_object_filename_or_none() -> None: + ''' + Test the :func:`beartype._util.utilobject.get_object_filename_or_none` + getter. + ''' + + # Defer test-specific imports. + from beartype._util.utilobject import get_object_filename_or_none + from beartype_test.a00_unit.data.data_type import ( + Class, + function, + ) + + # Assert this getter returns the expected filename for a physical class. + assert 'data_type' in get_object_filename_or_none(Class) + + # Assert this getter returns the expected filename for a physical callable. + assert 'data_type' in get_object_filename_or_none(function) + + # Assert this getter returns "None" when passed an object that is neither a + # class *NOR* callable. + assert get_object_filename_or_none(Class()) is None + + def test_get_object_name() -> None: ''' - Test usage of the - :func:`beartype._util.utilobject.get_object_name` getter. + Test the :func:`beartype._util.utilobject.get_object_name` getter. ''' # ....................{ IMPORTS }.................... diff --git a/beartype_test/a00_unit/a20_util/text/test_utiltextansi.py b/beartype_test/a00_unit/a20_util/text/test_utiltextansi.py new file mode 100644 index 00000000..9e50f34a --- /dev/null +++ b/beartype_test/a00_unit/a20_util/text/test_utiltextansi.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# --------------------( LICENSE )-------------------- +# Copyright (c) 2014-2023 Beartype authors. +# See "LICENSE" for further details. + +""" +**Beartype ANSI utility unit tests.** + +This submodule unit tests the public API of the private +:mod:`beartype._util.text.utiltextansi` submodule. +""" + + +# ....................{ IMPORTS }.................... +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +# WARNING: To raise human-readable test errors, avoid importing from +# package-specific submodules at module scope. +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +# ....................{ TESTS ~ tester }.................... +def test_is_text_ansi() -> None: + ''' + Test the :func:`beartype._util.text.utiltextansi.is_text_ansi` tester. + ''' + + # Defer test-specific imports. + from beartype._util.text.utiltextansi import is_text_ansi + + # Assert that this tester returns false for a string containing *NO* ANSI. + assert is_text_ansi('The sea-blooms and the oozy woods which wear') is False + + # Assert that this tester returns true for a string containing ANSI. + assert is_text_ansi('The sapless foliage of the ocean, know\033[92m') is ( + True) + +# ....................{ TESTS ~ stripper }.................... +def test_strip_text_ansi() -> None: + ''' + Test the :func:`beartype._util.text.utiltextansi.strip_text_ansi` stripper. + ''' + + # Defer test-specific imports. + from beartype._util.text.utiltextansi import strip_text_ansi + + # String containing *NO* ANSI. + THY_VOICE = 'and suddenly grow gray with fear,' + + # Assert that this stripper preserves strings containing *NO* ANSI as is. + assert strip_text_ansi(THY_VOICE) == THY_VOICE + + # Assert that this stripper strips *ALL* ANSI from strings containing ANSI. + assert strip_text_ansi(f'\033[31m{THY_VOICE}\033[92m') == THY_VOICE diff --git a/beartype_test/a00_unit/data/data_type.py b/beartype_test/a00_unit/data/data_type.py index 444cd57b..46b11ca9 100644 --- a/beartype_test/a00_unit/data/data_type.py +++ b/beartype_test/a00_unit/data/data_type.py @@ -11,16 +11,17 @@ ''' # ....................{ IMPORTS }.................... -from contextlib import contextmanager -from enum import Enum -from sys import exc_info -from typing import ( +from beartype.typing import ( AsyncGenerator, Callable, ContextManager, Coroutine, Generator, ) +from beartype._util.func.utilfuncmake import make_func +from contextlib import contextmanager +from enum import Enum +from sys import exc_info # ....................{ CLASSES }.................... class CallableClass(object): @@ -185,6 +186,31 @@ class NonIssubclassableClass(object, metaclass=NonIssubclassableMetaclass): pass +# ....................{ CLASSES ~ with : module name }.................... +class ClassWithModuleNameNone(object): + ''' + Arbitrary pure-Python class with a **missing module name** (i.e., whose + ``__module__`` dunder attribute is :data:`None`). + ''' + + pass + + +class ClassWithModuleNameNonexistent(object): + ''' + Arbitrary pure-Python class with a **non-existent module name** (i.e., whose + ``__module__`` dunder attribute refers to a file that is guaranteed to *not* + exist on the local filesystem). + ''' + + pass + + +# Monkey-patch the above classes with "bad" module names. +ClassWithModuleNameNone.__module__ = None +ClassWithModuleNameNonexistent.__module__ = ( + 'If_I.were.a_dead_leaf.thou_mightest.bear') + # ....................{ CALLABLES ~ async : factory }.................... # Note that we intentionally avoid declaring a factory function for deprecated # generator-based coroutines decorated by either the types.coroutine() or @@ -242,12 +268,23 @@ async def async_generator_factory(text: str) -> AsyncGenerator[str, None]: # ....................{ CALLABLES ~ sync }.................... def function(): ''' - Arbitrary pure-Python function. + Arbitrary pure-Python function physically declared by this submodule. ''' pass +function_in_memory = make_func( + func_name='function_in_memory', + func_code=''' +def function_in_memory(): + return 'And tremble and despoil themselves: oh hear!' +''', + func_doc=''' +Arbitrary pure-Python function dynamically declared in-memory. +''') + + @contextmanager def context_manager_factory(obj: object) -> ContextManager[object]: ''' diff --git a/beartype_test/a00_unit/data/util/func/data_utilfunccode.py b/beartype_test/a00_unit/data/util/func/data_utilfunccode.py index 39047efb..929e8cea 100644 --- a/beartype_test/a00_unit/data/util/func/data_utilfunccode.py +++ b/beartype_test/a00_unit/data/util/func/data_utilfunccode.py @@ -6,8 +6,8 @@ ''' **Beartype generic callable code data submodule.** -This submodule predefines low-level class constants exercising known edge -cases on behalf of the higher-level +This submodule predefines low-level class constants exercising known edge cases +on behalf of the higher-level :mod:`beartype_test.a00_unit.a20_util.func.test_utilfunccode` submodule. Unit tests defined in that submodule are sufficiently fragile that *no* other submodule should import from this submodule. diff --git a/doc/src/_links.rst b/doc/src/_links.rst index 2c3bd8ab..deaafccc 100644 --- a/doc/src/_links.rst +++ b/doc/src/_links.rst @@ -301,6 +301,8 @@ .. # ------------------( LINKS ~ py : package )------------------ .. _Django: https://www.djangoproject.com +.. _Hypothesis: + https://hypothesis.readthedocs.io .. _NetworkX: https://networkx.org .. _PyTorch: diff --git a/doc/src/faq.rst b/doc/src/faq.rst index 58c28869..e320a7bf 100644 --- a/doc/src/faq.rst +++ b/doc/src/faq.rst @@ -809,7 +809,7 @@ somebody refactors them into the first choice: #. **[Recommended]** The :pep:`673`\ -compliant :obj:`typing.Self` type hint (introduced by Python 3.11) efficiently and reliably solves this. Annotate the type of the current class as :obj:`~typing.Self` – fully supported by - :mod:`beartype` for all your greedy QA needs: + :mod:`beartype`: .. code-block:: python @@ -836,7 +836,7 @@ somebody refactors them into the first choice: :obj:`~typing.Self` is only contextually valid inside class declarations. :mod:`beartype` raises an exception when you attempt to use :obj:`~typing.Self` outside a class declaration (e.g., annotating a global - variable, function parameter, or function return). + variable, function parameter, or return). :obj:`~typing.Self` can only be type-checked by **classes** decorated by the :func:`beartype.beartype` decorator. Corollary: :obj:`~typing.Self` @@ -867,8 +867,9 @@ somebody refactors them into the first choice: #. A :pep:`563`\ -compliant **postponed type hint** (i.e., type hint unparsed by ``from __future__ import annotations`` back into a string that is the unqualified name of the current class) also resolves this. The only costs are - codebase-shattering inefficiency and unreliability. Only do this over the - rotting corpse of :mod:`beartype`. This is... + codebase-shattering inefficiency, non-deterministic fragility so profound + that even Hypothesis_ is squinting, and the ultimate death of your business + model. Only do this over the rotting corpse of :mod:`beartype`. This is... .. code-block:: python @@ -895,13 +896,15 @@ prefer :obj:`~typing.Self`. Why? **Speed.** It's why we're here. Let's quietly admit that to ourselves. If :mod:`beartype` were any slower, even fewer people would be reading this. -:mod:`beartype` generates optimally efficient type-checking code for -:obj:`~typing.Self`. It's literally just a trivial call to the -:func:`isinstance` builtin. The same can't be said for forward references or -postponed type hints, however. :mod:`beartype` generates suboptimal -type-checking code for both by deferring the lookup of the referenced class to -call time; although :mod:`beartype` caches that class after doing so, all of -that incurs space and time costs you'd rather not pay at any space or time. +:mod:`beartype` generates: + +* Optimally efficient type-checking code for :obj:`~typing.Self`. It's literally + just a trivial call to the :func:`isinstance` builtin. The same *cannot* be + said for... +* Suboptimal type-checking code for both forward references and postponed type + hints, deferring the lookup of the referenced class to call time. Although + :mod:`beartype` caches that class after doing so, all of that incurs space and + time costs you'd rather not pay at any space or time. :obj:`typing.Self`: it saved our issue tracker from certain doom. Now, it will save your codebase from our issues. diff --git a/doc/src/pep.rst b/doc/src/pep.rst index 1ace048a..0852dbef 100644 --- a/doc/src/pep.rst +++ b/doc/src/pep.rst @@ -82,7 +82,7 @@ you into stunned disbelief that somebody typed all this. [#rsi]_ +------------------------+-----------------------------------------------+---------------------------+---------------------------+ | | :pep:`613 <613>` | *none* | *none* | +------------------------+-----------------------------------------------+---------------------------+---------------------------+ - | | :pep:`621 <621>` | **0.14.0**\ —\ *current* | **0.14.0**\ —\ *current* | + | | :pep:`621 <621>` | **0.15.0**\ —\ *current* | **0.15.0**\ —\ *current* | +------------------------+-----------------------------------------------+---------------------------+---------------------------+ | | :pep:`646 <646>` | *none* | *none* | +------------------------+-----------------------------------------------+---------------------------+---------------------------+