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

Migrate from Typer to Cyclopts #79

Draft
wants to merge 22 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: [3.8, 3.9, "3.10", "3.11", "3.12", "3.13"]
python-version: [3.9, "3.10", "3.11", "3.12", "3.13"]

steps:
- uses: actions/checkout@v4
Expand Down
12 changes: 8 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

This release involves major changes to reprexlite. There is a significant refactoring of the library internals and also many changes to the API. This enabled new feature and more customizability.

_This release also removes support for Python 3.6, 3.7, and 3.8._

### CLI and IPython User Interfaces

#### Added
Expand All @@ -17,6 +19,7 @@ This release involves major changes to reprexlite. There is a significant refact
- A new `--parsing-method` option controls input-parsing behavior.
- The default value `auto` can automatically handle "reprex-style" input as well as "doctest-style`/Python REPL input.
- A value `declared` will use the values of `--prompt`, `--continuation`, and `--comment` for parsing input in addition to styling output. To handle input and output with different styes, you can override input-side values with the `--input-prompt`, `--input-continuation`, and `--input-comment` options.
- Added support for configuration files, including support for `[tool.reprexlite]` in `pyproject.toml` files and for user-level configuration. See ["Configuration"](https://jayqi.github.io/reprexlite/stable/configuration/#configuration-files) for more details.

#### Changed

Expand Down Expand Up @@ -45,9 +48,9 @@ This release involves major changes to reprexlite. There is a significant refact
#### Changed

- Changed formatting abstractions in `reprexlite.formatting` module.
- Rather than `*Reprex` classes that encapsulate reprex data, we now have `*Formatter` classes and take a rendered reprex output string as input to a `format` class method that appropriately prepares the reprex output for a venue, such as adding venue-specific markup.
- The `venues_dispatcher` dictionary in `reprexlite.formatting` is now a `formatter_registry` dictionary.
- Formatters are added to the registry using a `register_formatter` decorator instead of being hard-coded.
- Rather than `*Reprex` classes that encapsulate reprex data, we now have formatter callables and take a rendered reprex output string as input and appropriately prepares the reprex output for a venue, such as adding venue-specific markup.
- The `venues_dispatcher` dictionary in `reprexlite.formatting` is now a `formatter_registry` dictionary-like.
- Formatters are added to the registry using a `formatter_registry.register` decorator instead of being hard-coded.

#### Removed

Expand All @@ -58,8 +61,9 @@ This release involves major changes to reprexlite. There is a significant refact

#### Added

- Added a "Rendering and Output Venues" page to the documentation that documents the different formatting options with examples.
- Added a "Configuration" page to the documentation that provides a reference for configuration options and documents how to use configuration files.
- Added an "Alternatives" page to the documentation that documents alternative tools.
- Added a "Venues Formatting" page to the documentation that documents the different formatting options with examples.

#### Changed

Expand Down
17 changes: 13 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
[![tests](https://github.com/jayqi/reprexlite/workflows/tests/badge.svg?branch=main)](https://github.com/jayqi/reprexlite/actions?query=workflow%3Atests+branch%3Amain)
[![codecov](https://codecov.io/gh/jayqi/reprexlite/branch/main/graph/badge.svg)](https://codecov.io/gh/jayqi/reprexlite)

**reprexlite** is a tool for rendering **repr**oducible **ex**amples of Python code for sharing. With a [convenient CLI](#command-line-interface) and lightweight dependencies, you can quickly get it up and running in any virtual environment. It has an optional [IPython extension with cell magic](#ipythonjupyter-cell-magic) for easy use in Jupyter or VS Code. This project is inspired by R's [reprex](https://reprex.tidyverse.org/) package.
**reprexlite** is a tool for rendering **repr**oducible **ex**amples of Python code for sharing. With a [convenient CLI](#command-line-interface) and lightweight dependencies, you can quickly get it up and running in any virtual environment. It has an optional [integration with IPython](#ipython-integrations) for easy use with IPython or in Jupyter or VS Code. This project is inspired by R's [reprex](https://reprex.tidyverse.org/) package.

<img src="https://raw.githubusercontent.com/jayqi/reprexlite/main/docs/docs/images/demo.gif" width="640px" />

### What it does

- Paste or type some Python code that you're interested in sharing.
- reprexlite will execute that code in an isolated namespace. Any returned values or standard output will be captured and displayed as comments below their associated code.
- The rendered reprex will be printed for you to share. Its format can be easily copied, pasted, and run as-is by someone else. Here's an example of an outputted reprex:
Expand All @@ -26,7 +28,7 @@ list(zip(*grid))
#> [(1, 1, 2, 2, 3, 3), (8, 16, 8, 16, 8, 16)]
```

Writing a good reprex takes thought and effort (see ["Reprex Do's and Don'ts"](https://jayqi.github.io/reprexlite/stable/dos-and-donts) for tips). The goal of reprexlite is to be a tool that seamlessly handles the mechanical stuff, so you can devote your full attention to the important, creative work of writing the content.
Writing a good reprex takes thought and effort (see ["Reprex Do's and Don'ts"](https://jayqi.github.io/reprexlite/stable/dos-and-donts/) for tips). The goal of reprexlite is to be a tool that seamlessly handles the mechanical stuff, so you can devote your full attention to the important, creative work of writing the content.

Reprex-style code formatting—namely, with outputs as comments—is also great for documentation. Users can copy and run with no modification. Consider using reprexlite when writing your documentation instead of copying code with `>>>` prompts from an interactive Python shell. In fact, reprexlite can parse code with `>>>` prompts and convert it into a reprex for you instead.

Expand All @@ -48,7 +50,7 @@ Optional dependencies can be specified using the ["extras" mechanism](https://pa

- `black` : for optionally autoformatting your code
- `ipython` : to use the IPython interactive shell editor or `%%reprex` IPython cell magic
- `pygments` : for syntax highlighting and the RTF venue
- `pygments` : for syntax highlighting and rendering the output as RTF

### Development version

Expand All @@ -74,11 +76,18 @@ Once you're done, reprexlite will print out your reprex to console.

To see available options, use the `--help` flag.

### IPython integrations

There are two kinds of IPython integration:

1. [IPython interactive shell editor](#ipython-interactive-shell-editor), which opens up a special IPython session where all cells are run through reprexlite
2. [Cell magics](#ipythonjupyter-cell-magic), which let you designate individual cells in a normal IPython or Jupyter notebook for being run through reprexlite

### IPython interactive shell editor

_Requires IPython._ `[ipython]`

reprexlite optionally supports an IPython interactive shell editor. This is basically like a normal IPython interactive shell except that all cell contents are piped through reprexlite for rendering instead of the normal cell execution. It has the typical advantages of using IPython like auto-suggestions, history scrolling, and syntax highlighting. You can start the IPython editor by using the `--editor`/`-e` option:
reprexlite optionally supports an IPython interactive shell editor. This is basically like a normal IPython interactive shell except that all cells are piped through reprexlite for rendering instead of the normal cell execution. It has the typical advantages of using IPython like auto-suggestions, history scrolling, and syntax highlighting. You can start the IPython editor by using the `--editor`/`-e` option:

```bash
reprex -e ipython
Expand Down
45 changes: 45 additions & 0 deletions docs/docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,49 @@

reprexlite has the following configuration options.

> [!NOTE]
> Command-line option names for these configuration variables use hyphens instead of underscores.

{{ create_config_help_table() }}

## Configuration files

reprexlite supports reading default configuration values from configuration files. Both project-level files and user-level files are supported.

### `pyproject.toml`

reprexlite will search the nearest `pyproject.toml` file in the current working directory and any parent directory.
Configuration for reprexlite should be in the `[tool.reprexlite]` table following standard `pyproject.toml` specifications. For example:

```toml
[tool.reprexlite]
editor = "some_editor"
```

### `reprexlite.toml` or `.reprexlite.toml`

reprexlite also supports files named `reprexlite.toml` or `.reprexlite.toml` for project-level configuration. It will also search for these in the current working directory or any parent directory.

For reprexlite-specific files, all configuration options should be declared in the root namespace.

```toml
editor = "some_editor"
```

### User-level configuration

reprexlite supports searching standard platform-specific user configuration directories as determined by [platformdirs](https://github.com/tox-dev/platformdirs). Here are typical locations depending on platform:

| Platform | Path |
|----------|------------------------------------------------------------|
| Linux | `~/.config/reprexlite/config.toml` |
| MacOS | `~/Library/Application Support/reprexlite/config.toml` |
| Windows | `C:\Users\<username>\AppData\Local\reprexlite\config.toml` |

You can check where your user configuration would be with

```bash
python -m platformdirs
```

Look for the section `-- app dirs (without optional 'version')` for the value of `user_config_dir`. The value for `MyApp` is `reprexlite`. The configuration file should be named `config.toml` inside that directory.
4 changes: 2 additions & 2 deletions docs/docs/design-philosophy.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ A widely used approach for Python code examples is copying from an interactive P

This style of code example takes no special tools to generate: simply open a `python` shell from command line, write your code, and copy what you see. Many Python packages use it for their documentation, e.g., [requests](https://requests.readthedocs.io/en/master/). There is also tooling for parsing it. The doctest module can run such examples in the docstrings of your scripts, and test that the output matches what is written. Other tools like [Sphinx](https://www.sphinx-doc.org/en/1.4.9/markup/code.html) are able to parse it when rendering your documentation into other formats.

The drawback of doctest-style examples is that they are _not_ valid Python syntax, so you can't just copy, paste, and run such examples. The `>>>` prompt is not valid. While IPython's interactive shell and Jupyter notebooks _do_ support executing code with the prompt, it won't work in a regular Python REPL or in Python scripts. Furthermore, since the outputs might be anything, they may not be valid Python syntax either, depending on their `repr`. A barebones class, for example, will look like `<__main__.MyClass object at 0x7f932a001400>` and is not valid.
The drawback of doctest-style examples is that they are _not_ valid Python syntax, so you can't always just copy, paste, and run such examples. The `>>>` prompt is not valid. While IPython's interactive shell and Jupyter notebooks _do_ support executing code with the prompt, it won't work in a regular Python REPL or in Python scripts. Furthermore, since the outputs might be anything, they may not be valid Python syntax either, depending on their `repr`. A barebones class, for example, will look like `<__main__.MyClass object at 0x7f932a001400>` and is not valid.

So, while no special tools were needed to _generate_ a doctest-style example, either special tools or manual editing are needed to _run_ it. This puts the burden on the person you're sharing with, which is counterproductive. As discussed in the previous section, we want reproducible examples to make it _easier_ for others to run your code.

Expand All @@ -56,7 +56,7 @@ If this has convinced you, you can take advantage of reprexlite's ability to par

The primary design goal of reprexlite is that it should be **quick and convenient** to use. That objective drove the emphasis on following the design characteristics:

- **Lightweight**. reprexlite needs to be in your virtual environment to be able to run your code. By having minimal and lightweight dependencies itself, reprexlite is quick to install and is unlikely to conflict with your other dependencies.
- **Lightweight**. reprexlite needs to be in your virtual environment to be able to run your code. By having minimal and lightweight dependencies itself, reprexlite is quick to install and is unlikely to conflict with your other dependencies. Any advanced functionality that require heavier dependencies are optional.
- **Easy access**. reprexlite comes with a CLI, so you can quickly create a reprex without needing to start a Python shell or to import anything.
- **And flexible**. The CLI isn't the only option. The [Python library](../api-reference/reprex/) provides programmatic access, and there is an optional [IPython/Jupyter extension](../ipython-jupyter-magic/) for use with a cell magic.

Expand Down
33 changes: 0 additions & 33 deletions docs/docs/formatting.md

This file was deleted.

29 changes: 29 additions & 0 deletions docs/docs/rendering-and-output-venues.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Rendering and Output Venues

A rendered reprex will be code plus the computed outputs plus additional formatting markup appropriate for some particular output venue. For example, the `gh` venue (GitHub) will be in GitHub-flavored markdown and may look like this:

````
```python
2+2
#> 4
```
````

The venue can be set using the `--venue / -v` command-line flag or the `venue` configuration file option. The following section documents the available output venue options.

## Venue options

{{ create_venue_help_table() }}

## Formatter functions

{{ create_venue_help_examples() }}

## Under the hood and Python API

There are two steps to rendering a reprex:

1. The `Reprex.render()` method renders a reprex instance as just the code and outputs.
2. A formatter function from `reprexlite.formatting` (see [above](#formatter-functions)) formats the rendered reprex code and outputs for the specified venue.

The whole process is encapsulated in the `Reprex.render_and_format()` method.
68 changes: 41 additions & 27 deletions docs/main.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from collections import defaultdict
import dataclasses
from textwrap import dedent
from typing import Union

from markdownTable import markdownTable
from griffe import Docstring, DocstringSectionAdmonition, DocstringSectionText
from py_markdown_table.markdown_table import markdown_table
from typenames import typenames

from reprexlite.config import ReprexConfig
Expand All @@ -12,6 +14,10 @@
def define_env(env):
"Hook function"

docstring = Docstring(ReprexConfig.__doc__, lineno=1)
parsed = docstring.parse("google")
descriptions = {param.name: param.description.replace("\n", " ") for param in parsed[1].value}

@env.macro
def create_config_help_table():
out = dedent(
Expand All @@ -34,7 +40,7 @@ def create_config_help_table():
<tr>
<td class="no-wrap"><b><code>{field.name}</code></b></td>
<td class="no-wrap"><code>{typenames(field.type)}</code></td>
<td>{field.metadata['help']}</td>
<td>{descriptions[field.name]}</td>
</tr>
"""
for field in dataclasses.fields(ReprexConfig)
Expand All @@ -44,46 +50,54 @@ def create_config_help_table():
'"Venues Formatting"', '<a href="../formatting/">"Venues Formatting"</a>'
)
return out
# data = [
# {
# "Name": f"**`{field.name}`**",
# "Type": f"`{typenames(field.type)}`",
# "Description": field.metadata["help"],
# }
# for field in dataclasses.fields(ReprexConfig)
# ]
# table = markdownTable(data)
# return table.setParams(row_sep="markdown", quote=False).getMarkdown()

@env.macro
def create_venue_help_table():
data = [
{
"Venue Keyword": venue_key,
"Description": formatter.meta.venues[venue_key],
"Formatter": f"[`{formatter.__name__}`](#{formatter.__name__.lower()})",
"Venue Keyword": f"`{venue_key.value}`",
"Description": formatter_entry.label,
"Formatter Function": f"[`{formatter_entry.fn.__name__}`](#{formatter_entry.fn.__name__})",
}
for venue_key, formatter in formatter_registry.items()
for venue_key, formatter_entry in formatter_registry.items()
]
table = markdownTable(data)
return table.setParams(row_sep="markdown", quote=False).getMarkdown()
table = markdown_table(data)
return table.set_params(row_sep="markdown", quote=False).get_markdown()

@env.macro
def create_venue_help_examples():
data = defaultdict(list)
for key, entry in formatter_registry.items():
data[entry.fn].append(key)

out = []
for formatter in dict.fromkeys(formatter_registry.values()):
out.append(f"### `{formatter.__name__}`")
out.append("")
out.append(formatter.__doc__)
out.append("")
out.append("````")
out.append(formatter.meta.example or "Example not shown.")
out.append("````")
for fn, keys in data.items():
keys_list = ", ".join(f"`{key.value}`" for key in keys)
out.append(f"### `{fn.__name__}`")

# Parse docstring
docstring = Docstring(fn.__doc__, lineno=1)
parsed = docstring.parse("google")

for section in parsed:
if isinstance(section, DocstringSectionText):
out.append("")
out.append(section.value)
elif isinstance(section, DocstringSectionAdmonition):
out.append("")
out.append(f"**Used for venues**: {keys_list}")
out.append("")
out.append(f"**{section.title}**")
out.append("")
out.append("````")
admonition = section.value
out.append(admonition.description)
out.append("````")
out.append("")
out.append(
"<sup>"
"↳ [API documentation]"
f"(api-reference/formatting.md#reprexlite.formatting.{formatter.__qualname__})"
f"(api-reference/formatting.md#reprexlite.formatting.{fn.__qualname__})"
"</sup>"
)
return "\n".join(out)
Loading