Skip to content

Conversation

@gadenbuie
Copy link
Collaborator

@gadenbuie gadenbuie commented Jan 18, 2024

Historically, pre-quartodoc, we used an @add_example() decorator to explicitly add examples. When we moved to quartodoc, we switched to an automatic method for including examples and @add_example() has been effectively disabled since then.

This PR restores the decorator and adapts it for use in quartodoc. The decorator approach gives us a few advantages over the automatic example approach. In particular it's now possible to add more than one example and to use non-standard file names for the example app.

Behavior after this PR

  1. Use @add_example() without arguments to automatically add an example from api-examples/{fn_name}/app.py, where api-examples can be anywhere in the documented functions parent directories.

    • This lets us accomodate shiny/api-examples/{func}/app.py and shiny/api-examples/{func}/app-express.py and alternatively shiny/express/api-examples/{func}/app.py.
  2. Use the app_file argument of @add_example() to add an example from a file not named app.py

    • Decorators are applied bottom-up, but @add_examples() applies the example in the order they appear in the code. In the following, the basic example appears first, followed by the complicated example.

      @add_example("app-basic.py")
      @add_example("app-complicated.py")
      def page_navbar():
          ...
    • add_example() will add the other files in the example directory as supporting files, but it will not include app.py or files that start with app-.

  3. You can call @add_example() more than once. They should be grouped together in a block (called sequentially) and multiple examples will be collected under a single "Examples" heading.

  4. We now require that all public functions have an example and make quartodoc will throw if an included function doesn't have an example. You can use the @no_example decorator to tell quartodoc that the documented function intentionally doesn't include examples.

    1. This caught a few places where we have existing reasonable examples that weren't linked to a doc string, e.g. render.plot.
    2. Examples can be added using @add_example() or by manually including an Examples heading. NOTE: Examples must be plural!
    3. I've ignored shiny.express (temporarily) and shiny.experimental. We also don't require examples in external packages, e.g. from htmltools. I'll remove the shiny.express exclusion when we start adding those examples.

Changes in this PR

Setting this up required some large-ish changes to our quartodoc process.

  1. We now set dynamic: true in docs/_quartodoc.yml and selectively mark functions to be visited statically with markup like

    contents:
      - name: render.renderer.Jsonifiable
        dynamic: false
  2. @add_example() is a no-op unless SHINY_ADD_EXAMPLES is "true". When exactly "true", we now write an Examples section into the docs that only includes the main app file as a static Python chunk.

  3. When both SHINY_ADD_EXAMPLES and IN_QUARTODOC are "true", we wrap out the examples writer with a function that uses py-shinylive to create a shinylive quarto chunk. quartodoc may eventually set this envvar manually, for now we set both env vars in make quartodoc.

  4. I've kept the no-op behavior of @add_example(), but in my checking if we always allow @add_example() to run, import shiny goes from appox 0.09 seconds to 0.15. The utility is limited, however; in VSCode the function docs hover cards don't include the dynamically rendered portion of the docstrings.

Other notes and tasks

  • I committed the intermediate docs/api/*.qmd files at the start of this PR and periodically updated them throughout to provide a way to see the changes in the final result. Before merging we need to stop tracking these intermediate files.
  • See the note above about removing the example check in make quartodoc and the @no_example decorator.
    • Resolution: We now check that all exported functions have an example and use the @no_example decorator to indicate that no example is required for the function.
  • There are some type errors around using shinylive. I could use some help debugging these and finding the best pattern for conditionally loading and using shinylive.
    • Resolution: I hid the doc-related code from the type checker by placing it behind if not TYPE_CHECKING. This path is only executed when building quartdoc (i.e. when IN_QUARTODOC is "true") but is run frequently in our CI tests, so the risk of this solution is low.
  • I'm making use of the dev version of py-shinylive in this PR.

@gadenbuie gadenbuie requested a review from schloerke January 18, 2024 16:52
- render.renderer.ValueFn
- render.renderer.AsyncValueFn
- name: render.renderer.Jsonifiable
dynamic: false
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is it important that these are dynamic: false? For better performance, or correctness?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case and the other one in this section, dynamic: true leads to cyclic reference errors in griffe

@jcheng5 jcheng5 mentioned this pull request Jan 18, 2024
55 tasks
@gadenbuie gadenbuie merged commit 3691b35 into main Jan 19, 2024
@gadenbuie gadenbuie deleted the docs/example-app-file branch January 19, 2024 19:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants