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

Create a custom fence that allows for arbitrary Python code execution and display as a Python REPL #1690

Closed
facelessuser opened this issue May 11, 2022 · 4 comments
Labels
skip-triage Tells bot to not tag a new issue with 'triage'. T: feature Feature.

Comments

@facelessuser
Copy link
Owner

Description

This would allow users to create a special fence that accepts arbitrary Python code and then executes it and displays it as if it were run in a Python REPL.

Proof of concept found here: https://facelessuser.github.io/coloraide/

We would strip out color-specific things as those are unique to the linked project. Also, executing live in the browser is not the current goal, even if the linked project does this.

Related to mkdocs/mkdocs#2835

Benefits

For Python projects, this could be useful to provide real examples of code that could be executed and shown working. If the behavior of the example changes, it will be updated automatically. If the example ever fails, we could ensure that the fence throws an error (if desired) to prevent pushing out broken examples.

Solution Idea

Linked proof of concept.

@facelessuser facelessuser added T: feature Feature. skip-triage Tells bot to not tag a new issue with 'triage'. labels May 11, 2022
@gir-bot
Copy link
Collaborator

gir-bot commented May 11, 2022

Oops! It appears I am having difficulty marking this issue for triage.

@facelessuser
Copy link
Owner Author

facelessuser commented Feb 27, 2023

Actually have this working. Took the ColorAide idea and stripped out all the color stuff.

ColoAide required a bit more complexity as it needed to get a hold of actual color objects to generate previews and gradients and such, which means we really needed to execute the code ourselves. This required us to create the AST for the code and evaluate all the expressions, line by line. If/else, try/catch, for/else, while/else, and match/case blocks could not be evaluated in the normal way to extract the objects, so we had to create special logic to duplicate the block logic so we could then evaluate the expressions inside the blocks.

With all the color logic pulled out, we could simplify the code even further using code.InteractiveInterpreter. We still need to build an AST so we can tell the difference between expressions and blocks, this means we can allow people to drop natural code into the code blocks instead of having to ensure every block statement has a newline and such after them. If we removed the AST logic, people would constantly run into issues such as:

if this:
   ...
print('this causes an error because there is no empty line before this line')

So, at least identifying blocks allows us to intelligently process the code and avoid such problems.

The end result is far more simple than ColorAide, which makes sense as ColorAide requires far more intelligence to get at color objects.

Now, we can add the following which overrides the py highlight language.

  - pymdownx.superfences:
      preserve_tabs: true
      custom_fences:
        - name: py
          class: 'highlight'
          format: !!python/object/apply:tools.pycon.py_command_formatter
            kwds:
              init: |
                import markdown
          validator: !!python/name:tools.pycon.py_command_validator

When we specify a python block normally, it'll highlight like Python:

```py
text = "A link https://google.com"
markdown.markdown(text, extensions=['pymdownx.magiclink'])
```

Screenshot 2023-02-26 at 7 57 52 PM

But when we add the run argument, the code is evaluated and executed giving us a Python Console output:

```py run
text = "A link https://google.com"
markdown.markdown(text, extensions=['pymdownx.magiclink'])
```

Screenshot 2023-02-26 at 7 59 58 PM

The setup loads markdown in globally and then we code blocks with run are executed with markdown in globals.

We can even do some per block setup and hide the setup if we don't want it to be part of the example. We just have to wrap code at the very beginning of the block with # pragma: init

```py run
# pragma: init
text = "A link https://google.com"
# pragma: init
markdown.markdown(text, extensions=['pymdownx.magiclink'])
```

Now we get:

Screenshot 2023-02-26 at 8 03 02 PM

Pretty cool. I don't know if we'll ship the custom block directly in Pymdown Extensions or not. Maybe I'll test drive it in some other projects for now, but I'll link a gist sometime soon.

@facelessuser
Copy link
Owner Author

@facelessuser
Copy link
Owner Author

This has been created, but I'm not sure if it is going to live here or not. I will decide that at some later time. I'll try to update the gist with recent changes, but for now, this is has been investigated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
skip-triage Tells bot to not tag a new issue with 'triage'. T: feature Feature.
Projects
None yet
Development

No branches or pull requests

2 participants