Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature Request] A new utility function returning True only if "autodoc" is currently running #9805

Open
leycec opened this issue Nov 1, 2021 · 1 comment
Labels
extensions:autodoc type:enhancement enhance or introduce a new feature

Comments

@leycec
Copy link

leycec commented Nov 1, 2021

Is your feature request related to a problem? Please describe.

Firstly, my thanks to the tireless Sphinx crew (this means you!) for your dedication to the just cause of exhaustively well-documented Python APIs.

Secondly, hi! I'm @leycec, the author of @beartype, Python's constant-time runtime type-checking decorator. You can probably see where this is going already – but let's go there anyway.

Like most decorators, @beartype is typically applied at module scope. The autodoc extension imports the modules it imports, thus implicitly calling @beartype for each @beartype-decorated callable. This isn't a problem in and of itself. Problems arise, however, whenever a @beartype-decorated callable is annotated with one more classes mocked by autodoc_mock_imports. When that happens, @beartype raises exceptions at decoration time, because mocking subverts our assumptions and expectations about classes used as annotations.

Describe the solution you'd like

A new public utility function isautodoc() (or something) in the Sphinx API returning True only if autodoc is currently running. Given that, it would be trivial for @beartype to conditionally reduce to a noop (i.e., the identity decorator returning the passed callable undecorated) when autodoc is running.

I was quite surprised not to find such a function. There exist a multitude of StackOverflow posts on the subject – but none with anything resembling a satisfactory answer. For example:

  • This answer and this answer both suggest ad-hoc process argument parsing (e.g., Path(argv[0]).name == "sphinx-build" or Path(argv[0]).name == "build.py") to detect whether Sphinx is currently running. Of course, that's horrifyingly non-portable, fragile, and overly coarse-grained. Leveraging that test in @beartype would have the unintended side effect of prohibiting third-party Sphinx extension authors from type-checking their APIs with @beartype. Moreover, it fails to generalize to build scripts, systems, and process not satisfying that finite list of basenames. This can't be the way.
  • This answer and this answer suggest setting an application-specific global environment variable (e.g., SPHINX_BUILD, GENERATING_DOCS) from within a conf.py configuration and then subsequently detecting that variable from within a decorator. Of course, we can't do that; @beartype doesn't control third-party conf.py configurations.
  • This answer suggests setting a ridiculous builtins.__sphinx_build__ global dunder attribute (guaranteed to break the back of Python and/or Sphinx at some future date) from within a conf.py configuration and then subsequently detecting that variable from within a decorator. Of course, we can't do that; @beartype doesn't control third-party conf.py configurations.

Indeed, one of the above answers notes that:

I think the less surprising thing to go there is to put some explicit guard inside your "classmethod property cache" functions to not run when the file is being processed by sphinx. Since sphinx do not have this feature itself, you can use an environment variable for that...

In short, nothing works.

Describe alternatives you've considered

I briefly considered the Sphinx Event API. The autodoc extension fires off a number of autodoc-specific events. In theory, @beartype could connect an internal event handler to Sphinx's current app singleton on one or more of these events being fired – which would then notify @beartype that it should then reduce to a noop.

Of course, that's horribly convoluted, fragile, and non-trivial for something that should ideally just reduce to a trivial one-liner. I also have no idea how to actually implement that, because I have no idea how to retrieve Sphinx's current app singleton. I suppose I could violate sanity by searching up the call stack for a script named conf.py and then introspect the app attribute out of the stack frame encapsulating that call, but... now we've really violated sanity, because that wouldn't even be portable across Python interpreters.

Moreover, there isn't necessarily a one-to-one relationship between autodoc-specific events and whether or not autodoc is currently running. I can easily envision cases in which no autodoc-specific event fires – yet autodoc is running.

The fragile plenum of the @beartype API is now rupturing. 💔

Additional context

Thanks again, one and all. We cannot emphasize enough how amazing everyone here is. Viva la reStructuredText revolution! 🎆

@leycec leycec added the type:enhancement enhance or introduce a new feature label Nov 1, 2021
leycec added a commit to beartype/beartype that referenced this issue Nov 6, 2021
This patch release delivers obstreperous support for **Sphinx** (*curse
ye and yer little side effects too, `autodoc` extension!*), **generator
callables, readable exception messages,** and **Python 3.10 CI.**

This release resolves **4 issues** and merges **0 pull requests.**
Fearsome changes include:

## Compatibility Improved

* **Sphinx.** `@beartype` now explicitly provides first-class support
  for Sphinx's `autodoc` extension (i.e., `sphinx.ext.autodoc`),
  resolving issue #61, kindly submitted by SeldonIO/alibi ML dev maestro
  supremum Janis Klaise (@jklaise). `@beartype` now transparently
  ignores `autodoc`-mocked module attributes (e.g., produced by the
  `autodoc_mock_imports` list global in Sphinx-specific
  `doc{s,}/conf.py` configuration files of downstream documentation
  trees) used as type hints annotating `@beartype`-decorated callables,
  rendering `@beartype` compatible with `autodoc`-documented codebases.
  Since Sphinx lacks a public API for detecting when `autodoc` is
  currently generating documentation (see: sphinx-doc/sphinx#9805),
  `@beartype` now performs its own ad-hoc detection at decoration time
  with micro-optimized runtime complexity:
  * `O(1)` in the common case (i.e., Sphinx's `autodoc` extension has
    *not* been previously imported under the active Python interpreter).
  * `O(n)` in the worst case (i.e., Sphinx's `autodoc` extension has
    been previously imported under the active Python interpreter), where
    `n` is the height of the call stack leading to `@beartype`.
* **Generator callables.** `@beartype` now relaxes the requirement that
  [a]synchronous generator callables be annotated by [PEP 484][PEP 484]
  -compliant `typing.{Async,}Generator[...]` or [PEP
  585][PEP 585]-compliant `collections.abc.{Async,}Generator[...]` type
  hints to additionally accept [PEP 484][PEP 484]-compliant
  `typing.{Async,}Iterable[...]` and `typing.{Async,}Iterator[...]`
  as well as [PEP 585][PEP 585]-compliant
  `collections.abc.{Async,}Iterable[...]` and
  `collections.abc.{Async,}Iterator[...]` type hints, resolving issue
  #65 kindly submitted by the @posita the positronic brain emitter.

## Features Improved

* **Exception message readability.** `@beartype` now emits more
  human-readable, disambiguous, and useful exception messages. Notably:
  * `@beartype`-decorated callables annotated by one or more numeric
    types (e.g., `int`, `float`, `complex`) now raise exceptions on
    type-checking failures whose exception messages disambiguate those
    numbers from strings, resolving issue #63 kindly submitted by
    @jefcolbi. Previously, these messages double-quoted all numbers for
    disambiguity with the possibly preceding sequence index of those
    numbers in their parent containers -- which then introduced yet
    another unanticipated ambiguity between numbers and strings. Numbers
    are now preserve in their vanilla unquoted form; meanwhile, sequence
    items are additionally disambiguated from sequence values with
    additional explanatory substrings.
  * Edge-case exception messages emitted by our memoized code generator
    no longer contain the mostly inappropriate substring `"wrapper
    parameter" *except* where absolutely appropriate.
  * Annotation-centric exception messages now guaranteeably contain the
    critical substring `"type hint"`.

## Issues Resolved

* **#61,** kindly submitted by SeldonIO/alibi ML dev maestro supremum
  Janis Klaise (@jklaise). See "Compatibility Improved" above.
* **#62,** kindly submitted by Cal Leeming (@foxx), may his serious
  eyebrow-raising monochrome GitHub avatar be a guiding light to us
  all. See "Documentation Revised" below. (Thanks again to the foxy
  @foxx for appraising us all of hitherto unknown typing horrors. And
  just in time for Halloween too.)
* **#63,** kindly submitted by @jefcolbi. See "Features Improved" above.
* **#65,** kindly submitted by @posita the positronic brain emitter. See
  "Compatibility Improved" above.

## Tests Improved

* **Python 3.10 CI.** This release resurrects Python 3.10 support in our
  GitHub Actions-based continuous integration (CI) configuration,
  circumventing upstream issue actions/setup-python#160 by coercively
  double-quoting *all* Python version strings specified as the values of
  "python-version:" keys. *Ya!*

## Documentation Revised

* **Reverse dependency showcase.** The introduction of our front-facing
  `README.rst` documentation now gratefully advertises popular reverse
  dependencies (i.e., downstream open-source Python projects directly
  leveraging `beartype`) with a new on-brand icon bar. Gaze upon iconic
  icons and know the stylish face of unabashed beauty.
* **Type hint elision.** The new "Type Hint Connectives" subsection of
  our front-facing `README.rst` documentation documents various means of
  circumventing counter-intuitive historicity in Python's core type
  hierarchy with (*wait for it*) beartype validators, resolving issue
  #62 kindly submitted by Cal Leeming (@foxx). Specifically, this
  subsection documents how to effectively declare:
  * A new PEP-compliant `int - bool` type hint as a beartype validator
    matching the set of all integers that are *not* booleans.
  * A new PEP-compliant `Sequence - str` type hint as a beartype
    validator matching the set of all sequences that are *not* strings.

  [PEP 484]: https://www.python.org/dev/peps/pep-0484
  [PEP 585]: https://www.python.org/dev/peps/pep-0585

(*Unleaded laden jelly-like underbellies!*)
@john-hen
Copy link

To add another hack to the mix, we could also do the following to find out if our package is currently being imported as part of a Sphinx build:

try:
    import sphinx
    sphinx_build = hasattr(sphinx, 'application')
except ImportError:
    sphinx_build = False

This takes advantage of the fact that Sphinx imports next to nothing when we just do import sphinx. The application module would only be in the name space if an entry point had been called. We could also check for sphinx.cmd, might be a tad more robust.

Or check sphinx.ext.autodoc specifically. but I don't really see the point of that. It's best to find out if Sphinx is running as there might be other extensions (Autosummary, Apidoc) that import external code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
extensions:autodoc type:enhancement enhance or introduce a new feature
Projects
None yet
Development

No branches or pull requests

3 participants