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

MarkdownAIO #82

Closed
wants to merge 41 commits into from
Closed

MarkdownAIO #82

wants to merge 41 commits into from

Conversation

AnnMarieW
Copy link
Collaborator

@AnnMarieW AnnMarieW commented Jan 31, 2022

Adds the new component MarkdownAIO

See the live demo

MarkdownAIO is a Dash feature that allows you to write Dash Apps as Markdown files. Simply pass in a Markdown file and MarkdownAIO will return a set of components with the option to display and/or execute code blocks. So it’s part:

  • Documentation helper
  • Markdown / Text authoring tool
  • Easy way to bring a Markdown file into an app without doing open('file.md')

It's also compatible with the pages/ api. The online docs for dash-labs is a multi-page app made with pages/ and each page is a Markdown file displayed using MarkdownAIO.


Here is a summary of the TODOs and questions:

  • To further reduce security risk with exec, check for local files only. Ensure that the file is within a certain directory, and by default that should be the parent directory of the main app but maybe we could create a way to override that. Similar to /assets?
    Update - require full path from pages/

  • css:

    • create a MarkdownAIO stylesheet?
    • eliminated dbc dependency (replace Rows and Cols with inline css)
    • document the default style in subcomponents. Or better yet - use a stylesheet?
      Status: inprogress
  • see todos in pages.py in _register_page_from_markdown_file()

  • [x ] use UUID for clipboard ID?

  • add ability to change defaults "globally" so it doesn't have to be done for each MarkdownAIO() instance.

  • add ability to embed another file within the Markdown file. Use case being, ability to keep the python app code in a separate file so you can run it individually. We could integrate jinja in here perhaps…: {% include code.py %}

  • Add Markdown files to hot reload in dash. That way users can have the same hot-reloading dev experience when working in markdown

  • if code block is not executed don't register callbacks and layout? Goal it to reduce the number of id clashes.

  • Need to remove if __name__ == "__main__": ... from the code blocks?

  • refactor _update_props() (It works, but it's kinda ugly)

  • rewrite the _remove_app_instance() to use the AST module

@AnnMarieW AnnMarieW marked this pull request as ready for review January 31, 2022 17:21
Copy link
Member

@chriddyp chriddyp left a comment

Choose a reason for hiding this comment

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

looking awesome! several small changes requested. once those are made, i'll take a deeper look at the docs 🙂

dash_labs/dashdown.py Outdated Show resolved Hide resolved
dash_labs/dashdown.py Outdated Show resolved Hide resolved
dash_labs/dashdown.py Show resolved Hide resolved
dash_labs/dashdown.py Outdated Show resolved Hide resolved

- `exec_code` (boolean; default False):
If `True`, code blocks will be executed. This may also be set within the code block with the comment
# exec-code-true or # exec-code-false
Copy link
Member

Choose a reason for hiding this comment

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

describe where this comment should exist. perhaps include a full example with the triple backticks

Copy link
Collaborator Author

@AnnMarieW AnnMarieW Feb 2, 2022

Choose a reason for hiding this comment

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

It can actually be anywhere in the code block on a comment line. It's just that the prop won't be won't be displayed if it's included on the first line with the back ticks. Since there are 7 props that this applies to, maybe it would be better to have the full example in the docs instead of here.

dash_labs/dashdown.py Outdated Show resolved Hide resolved
dash_labs/dashdown.py Outdated Show resolved Hide resolved
dash_labs/dashdown.py Show resolved Hide resolved
dash_labs/plugins/pages.py Outdated Show resolved Hide resolved
dash_labs/dashdown.py Outdated Show resolved Hide resolved
dash_labs/dashdown.py Outdated Show resolved Hide resolved
side_by_side: True
---
"""
import frontmatter
Copy link
Member

Choose a reason for hiding this comment

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

I was curious to see how this library parsed YAML, given my exec / eval concerns when writing the code fence parser.

It looks like frontmatter depends on PyYAML's safe mode: https://github.com/eyeseast/python-frontmatter/blob/1c49958fdd504691fc842045425ea298316aa643/frontmatter/default_handlers.py#L224-L228

So then looking into PyYAML, I see some stuff like:

Note that the ability to construct an arbitrary Python object may be dangerous if you receive a YAML document from an untrusted source such as the Internet. The function yaml.safe_load limits this ability to simple Python objects like integers or lists.

from https://pyyaml.org/wiki/PyYAMLDocumentation

Then googled around yaml.safe_load and came across yaml/pyyaml#420

They fixed the issue which is great but the fact that an issue happened in the first place makes me feel a little concerned that there could be more issues lurking in that library.

So I might consider supporting a smaller subset of the YAML library and doing our own parsing with JSON, similar to what we did with the code fences.

I'll keep thinking a bit about this

dash_labs/plugins/pages.py Outdated Show resolved Hide resolved
@@ -401,6 +438,7 @@ def interpolate_index(**kwargs):
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
Copy link
Member

Choose a reason for hiding this comment

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

we might want to put this in the stylesheet that we document

dash_labs/dashdown.py Outdated Show resolved Hide resolved
if "app.layout" in code:
code = code.replace("app.layout", "layout")
if "layout" in code:
exec(code, scope)
Copy link
Member

@chriddyp chriddyp Feb 8, 2022

Choose a reason for hiding this comment

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

I'm feeling like we should lock this down a bit more and exclude folks from using exec within callbacks.
for users, this means:

  • MarkdownAIO can only be called outside of callbacks, meaning when the app starts up
  • All of the MarkdownAIO files would need to be written in advance of the app starting up

so this wouldn't be allowed:
pages/report.py

dash.register_page(__name__)
def layout():
    return MarkdownAIO('report.md', exec_code=True)

But this would be allowed:
pages/report.py

dash.register_page(__name__)
layout = MarkdownAIO('report.md', exec_code=True)

I can't think of many examples where this would be insufficient and it would prevent users from seemingly innocent but very insecure code like:

template_report.md

# Report for {customer}

`` `python
dcc.Graph(figure=px.scatter(...))
`` `

app.py

app.layout = html.Div([
    dcc.Dropdown(id='customer', options=['Acme', 'City']),
    html.Div(id='content')
])

@callback(Output('content', 'children'), Input('customer', 'value'))
def update(customer):
    with open('template_report.md', 'r') as f:
        content = f.read()
        
    customer_report = content.replace(customer=customer)
    with open('customer_report.md', 'w') as f:
        f.write(customer_report)
    return MarkdownAIO('customer_report.md', exec_code=True)

Copy link
Member

@chriddyp chriddyp Feb 8, 2022

Choose a reason for hiding this comment

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

The way to prevent exec from running in a callback would be to do:

def _exec(*args, **kwargs):
    # flask raises an error if flask.request.path is called outside of a request
    try:
        flask.request.path
    except RuntimeError as e:
        raise Exception('MarkdownAIO is being called with `exec_code=True` within a callback or flask request. This isn\'t supported. MarkdownAIO with exec can only be called when the app is starting')

    return exec(*args, **kwargs)

Copy link
Collaborator Author

@AnnMarieW AnnMarieW Feb 15, 2022

Choose a reason for hiding this comment

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

When I tried this it prevented all exec s in the app. Checking for flask.has_request_context() worked though.

Will this be overly restrictive with a multi-page app using path variables and query strings? Also, many pages are functions even if there are no variables passed to the layout.

Update: This only works if it's called from a .py file. If the MarkdownAIO is inside a callback in a .md file, then
the flask.has_request_context() is False

new format for props in code blocks
use ast to remove app instance
removed dbc dependency
@AnnMarieW AnnMarieW mentioned this pull request Feb 10, 2022
12 tasks
@AnnMarieW
Copy link
Collaborator Author

This was a cool project but did't make it across the finish line because there were concerns about executing the code securely.

There are some other community project that are good for project that are a mix of code and markdown text. See:

https://github.com/snehilvj/markdown2dash
https://github.com/snehilvj/dmc-docs
https://github.com/emilhe/dash-down

@AnnMarieW AnnMarieW closed this Jun 14, 2024
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.

2 participants