Skip to content

Commit

Permalink
beartype_this_package() exceptions improved.
Browse files Browse the repository at this point in the history
This commit significantly improves exceptions raised by the
`beartype.claw.beartype_this_package()` import hook for common use
cases, resolving both issue #320 kindly submitted by Copenhagen
superstar @komodovaran (Johannes Thomsen) and forum thread #330 kindly
submitted by long-time `typing` fiend @JWCS (Jude). Specifically,
`beartype_this_package()` now raises exception messages resembling
the following when called directly from:

* Top-level scripts:

  ```python
  beartype.roar.BeartypeClawHookUnpackagedException: Top-level script
  "/home/leycec/tmp/src/script.py" resides outside package structure.
  Consider calling another "beartype.claw" import hook. However, note that only
  other modules will be type-checked. "/home/leycec/tmp/src/script.py" itself
  will remain unchecked. All business logic should reside in submodules
  subsequently imported by "/home/leycec/tmp/src/script.py": e.g.,
      # Instead of this at the top of "/home/leycec/tmp/src/script.py"...
      from beartype.claw import beartype_this_package  # <-- you are here
      beartype_this_package()                          # <-- feels bad

      # ...pass the basename of the "src/" subdirectory explicitly.
      from beartype.claw import beartype_package  # <-- you want to be here
      beartype_package("src")  # <-- feels good

      from src.main_submodule import main_func  # <-- still feels good
      main_func()                   # <-- *GOOD*! "beartype.claw" type-checks this
      some_global: str = 0xFEEDFACE  # <-- *BAD*! "beartype.claw" ignores this
  This has been a message from your friendly neighbourhood bear.
  ```

* Top-level modules:

  ```python
  Top-level module "main.py" resides outside package structure but was
  *NOT* directly run as a script. "beartype.claw" import hooks require
  that modules either reside inside a package structure or be directly
  run as scripts. Since neither applies here, you are now off the deep
  end. @beartype no longer has any idea what is going on, sadly.
  Consider directly decorating classes and functions by the
  @beartype.beartype decorator instead: e.g.,
      # Instead of this at the top of "main"...
      from beartype.claw import beartype_this_package  # <-- you are here
      beartype_this_package()                          # <-- feels bad
  \n'
      # ...go old-school like it's 2017 and you just don't care.
      from beartype import beartype  # <-- you want to be here
      @beartype  # <-- feels good, yet kinda icky at same time
      def spicy_func() -> str: ...  # <-- *GOOD*! @beartype type-checks this
      some_global: str = 0xFEEDFACE  # <-- *BAD*! @beartype ignores this, but what can you do
  For your safety, @beartype will now crash and burn.
  ```

Unrelatedly, this commit also revises our ReadTheDocs (RTD)-hosted FAQ
entry on `pytest-beartype` to document configuration via standard
`pyproject.toml` files, resolving issue #327 kindly submitted by
spaghetti-loving Bay Area pasta guru @jamesbraza (James Braza).

(*Watery warts on a tart rotisserie!*)
  • Loading branch information
leycec committed Feb 20, 2024
1 parent f321589 commit 22787fb
Show file tree
Hide file tree
Showing 13 changed files with 349 additions and 172 deletions.
2 changes: 0 additions & 2 deletions beartype/_data/func/datafunc.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@
This private submodule is *not* intended for importation by downstream callers.
'''

# ....................{ IMPORTS }....................

# ....................{ SETS }....................
METHOD_NAMES_DUNDER_BINARY = frozenset((
'__add__',
Expand Down
25 changes: 25 additions & 0 deletions beartype/_data/func/datafunccodeobj.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env python3
# --------------------( LICENSE )--------------------
# Copyright (c) 2014-2024 Beartype authors.
# See "LICENSE" for further details.

'''
Project-wide **code object globals** (i.e., global constants describing code
objects of callables, classes, and modules).
This private submodule is *not* intended for importation by downstream callers.
'''

# ....................{ STRINGS }....................
FUNC_CODEOBJ_NAME_MODULE = '<module>'
'''
Arbitrary string constant unconditionally assigned to the ``co_name`` instance
variables of the code objects of all pure-Python modules (i.e., the top-most
lexical scope of each module in the current call stack).
This constant enables callers to reliably differentiate between code objects
encapsulating:
* Module scopes, whose ``co_name`` variable is this constant.
* Callable scopes, whose ``co_name`` variable is *not* this constant.
'''
4 changes: 2 additions & 2 deletions beartype/_data/module/datamodcontextlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

# ....................{ IMPORTS }....................
from beartype.typing import Iterator
from beartype._util.func.utilfunccodeobj import get_func_codeobj_name
from beartype._util.func.utilfunccodeobj import get_func_codeobj_basename
from contextlib import contextmanager

# ....................{ STRINGS }....................
Expand All @@ -26,7 +26,7 @@ def _noop_context_manager() -> Iterator[None]:
yield


CONTEXTLIB_CONTEXTMANAGER_CODEOBJ_NAME = get_func_codeobj_name(
CONTEXTLIB_CONTEXTMANAGER_CODEOBJ_NAME = get_func_codeobj_basename(
_noop_context_manager)
'''
Fully-qualified name of the code object underlying the isomorphic decorator
Expand Down
9 changes: 7 additions & 2 deletions beartype/_data/module/datamodpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,17 @@
This private submodule is *not* intended for importation by downstream callers.
'''

# ....................{ IMPORTS }....................

# ....................{ NAMES }....................
BUILTINS_MODULE_NAME = 'builtins'
'''
Fully-qualified name of the **builtins module** (i.e., objects defined by the
standard :mod:`builtins` module and thus globally available by default
*without* requiring explicit importation).
'''


SCRIPT_MODULE_NAME = '__main__'
'''
Fully-qualified name of the **script module** (i.e., arbitrary module name
assigned to scripts run outside of a package context).
'''
4 changes: 2 additions & 2 deletions beartype/_util/func/mod/utilfuncmodtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from beartype._data.hint.datahintfactory import TypeGuard
from beartype._util.func.utilfunccodeobj import (
get_func_codeobj_or_none,
get_func_codeobj_name,
get_func_codeobj_basename,
)
from beartype._util.py.utilpyversion import IS_PYTHON_AT_MOST_3_10
from collections.abc import (
Expand Down Expand Up @@ -91,7 +91,7 @@ def is_func_contextlib_contextmanager(func: Any) -> TypeGuard[Callable]:
CONTEXTLIB_CONTEXTMANAGER_CODEOBJ_NAME)

# Fully-qualified name of that code object.
func_codeobj_name = get_func_codeobj_name(func_codeobj)
func_codeobj_name = get_func_codeobj_basename(func_codeobj)

# Return true only if the fully-qualified name of that code object is that
# of the isomorphic decorator closure created and returned by the standard
Expand Down
38 changes: 13 additions & 25 deletions beartype/_util/func/utilfunccodeobj.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,6 @@
MethodType,
)

# ....................{ CONSTANTS }....................
#FIXME: Shift into the "beartype._date" subpackage somewhere, please.
#FIXME: Unit test us up, please.
FUNC_CODEOBJ_NAME_MODULE = '<module>'
'''
String constant unconditionally assigned to the ``co_name`` instance variables
of the code objects of all pure-Python modules.
'''

# ....................{ GETTERS }....................
def get_func_codeobj(
# Mandatory parameters.
Expand Down Expand Up @@ -109,12 +100,12 @@ def get_func_codeobj(
the event of a fatal error. Defaults to the empty string.
Returns
----------
-------
CodeType
Code object underlying this codeobjable.
Raises
----------
------
exception_cls
If this codeobjable has *no* code object and is thus *not* pure-Python.
'''
Expand Down Expand Up @@ -193,7 +184,7 @@ def get_func_codeobj_or_none(
for both efficiency and disambiguity.
Returns
----------
-------
Optional[CodeType]
Either:
Expand All @@ -202,7 +193,7 @@ def get_func_codeobj_or_none(
* Else, :data:`None`.
See Also
----------
--------
:func:`.get_func_codeobj`
Further details.
'''
Expand Down Expand Up @@ -292,21 +283,18 @@ def get_func_codeobj_or_none(

# ....................{ GETTERS }....................
#FIXME: Unit test us up, please.
def get_func_codeobj_name(func: Codeobjable, **kwargs) -> str:
def get_func_codeobj_basename(func: Codeobjable, **kwargs) -> str:
'''
Fully-qualified name or unqualified basename (contextually depending on the
version of the active Python interpreter) of the passed **codeobjable**
(i.e., pure-Python object directly associated with a code object) if this
object is codeobjable *or* raise an exception otherwise (e.g., if this
object is *not* codeobjable).
Unqualified basename (contextually depending on the version of the active
Python interpreter) of the passed **codeobjable** (i.e., pure-Python object
directly associated with a code object) if this object is codeobjable *or*
raise an exception otherwise (e.g., if this object is *not* codeobjable).
Specifically, this getter returns:
* If the active Python interpreter targets Python >= 3.11 and thus defines
the ``co_qualname`` attribute on code objects, the value of that attribute
on the code object providing the fully-qualified name of this codeobjable.
* Else, the value of the ``co_name`` attribute on the code object providing
the unqualified basename of this codeobjable.
* If the active Python interpreter targets Python >= 3.11, the value of the
the ``co_qualname`` attribute on this code object.
* Else, the value of the ``co_name`` attribute on this code object.
Parameters
----------
Expand All @@ -317,7 +305,7 @@ def get_func_codeobj_name(func: Codeobjable, **kwargs) -> str:
:func:`.get_func_codeobj` getter.
Raises
----------
------
exception_cls
If this codeobjable has *no* code object and is thus *not* pure-Python.
'''
Expand Down
29 changes: 13 additions & 16 deletions beartype/_util/func/utilfuncfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@

# ....................{ IMPORTS }....................
from beartype.typing import Optional
from beartype._data.hint.datahinttyping import Codeobjable
from beartype._util.func.utilfunccodeobj import get_func_codeobj_or_none
from collections.abc import Callable
from linecache import cache as linecache_cache

# ....................{ TESTERS }....................
def is_func_file(func: Callable) -> bool:
def is_func_file(func: Codeobjable) -> bool:
'''
:data:`True` only if the passed callable is defined **on-disk** (e.g., by a
script or module whose pure-Python source code is accessible to the active
Expand All @@ -53,11 +53,11 @@ def is_func_file(func: Callable) -> bool:
Parameters
----------
func : Callable
Callable to be inspected.
func : Codeobjable
Codeobjable to be inspected.
Returns
----------
-------
bool
:data:`True` only if the passed callable is defined on-disk.
'''
Expand All @@ -66,13 +66,7 @@ def is_func_file(func: Callable) -> bool:
return get_func_filename_or_none(func) is not None

# ....................{ GETTERS }....................
def get_func_filename_or_none(
# Mandatory parameters.
func: Callable,

# Optional parameters.
# exception_cls: TypeException = _BeartypeUtilCallableException,
) -> Optional[str]:
def get_func_filename_or_none(func: Codeobjable, **kwargs) -> 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
Expand All @@ -82,11 +76,14 @@ def get_func_filename_or_none(
Parameters
----------
func : Callable
Callable to be inspected.
func : Codeobjable
Codeobjable to be inspected.
All remaining keyword parameters are passed as is to the
:func:`beartype._util.func.utilfunccodeobj.get_func_codeobj` getter.
Returns
----------
-------
Optional[str]
Either:
Expand Down Expand Up @@ -122,7 +119,7 @@ def get_func_filename_or_none(
# approach would unconditionally return the C-specific placeholder string
# for all callables -- including those originally declared as pure-Python in
# a Python module. So it goes.
func_codeobj = get_func_codeobj_or_none(func)
func_codeobj = get_func_codeobj_or_none(func, **kwargs)

# If the passed callable has *NO* code object and is thus *NOT* pure-Python,
# that callable was *NOT* defined by a pure-Python source code file. In this
Expand Down

0 comments on commit 22787fb

Please sign in to comment.