Preview GitHub README.md files locally before committing them.
Render local readme files before sending off to GitHub.
Grip is a command-line server application written in Python that uses the GitHub markdown API to render a local readme file. The styles and rendering come directly from GitHub, so you'll know exactly how it will appear. Changes you make to the Readme will be instantly reflected in the browser without requiring a page refresh.
Sometimes you just want to see the exact readme result before committing and pushing to GitHub.
Especially when doing Readme-driven development.
To install grip, simply:
$ pip install grip
On OS X, you can also install with Homebrew:
$ brew install grip
To render the readme of a repository:
$ cd myrepo $ grip * Running on http://localhost:6419/
Now open a browser and visit http://localhost:6419. Or run with
-band Grip will open a new browser tab for you.
You can also specify a port:
$ grip 80 * Running on http://localhost:80/
Or an explicit file:
$ grip AUTHORS.md * Running on http://localhost:6419/
Alternatively, you could just run
gripand visit localhost:6419/AUTHORS.md since grip supports relative URLs.
You can combine the previous examples. Or specify a hostname instead of a port. Or provide both.
$ grip AUTHORS.md 80 * Running on http://localhost:80/
$ grip CHANGES.md 0.0.0.0 * Running on http://0.0.0.0:6419/
$ grip . 0.0.0.0:80 * Running on http://0.0.0.0:80/
You can even bypass the server and export to a single HTML file, with all the styles and assets inlined:
$ grip --export Exporting to README.html
Control the output name with the second argument:
$ grip README.md --export index.html Exporting to index.html
If you're exporting a bunch of files, you can prevent styles from being inlining to save space with
--no-inline:
$ grip README.md --export --no-inline introduction.html Exporting to introduction.html
Reading and writing from stdin and stdout is also supported, allowing you to use Grip with other programs:
$ cat README.md | grip - * Running on http://localhost:6419/
$ grip AUTHORS.md --export - | bcat
$ cat README.md | grip --export - | less
This allows you to quickly test how things look by entering Markdown directly in your terminal:
$ grip - Hello **world**! ^D * Running on http://localhost:6419/
Note:
^Dmeans
Ctrl+D, which works on Linux and OS X. On Windows you'll have to use
Ctrl+Z.
Rendering as user-content like comments and issues is also supported, with an optional repository context for linking to issues:
$ grip --user-content --context=joeyespo/grip * Running on http://localhost:6419/
For more details and additional options, see the help:
$ grip -h
Grip strives to be as close to GitHub as possible. To accomplish this, grip uses GitHub's Markdown API so that changes to their rendering engine are reflected immediately without requiring you to upgrade grip. However, because of this you may hit the API's hourly rate limit. If this happens, grip offers a way to access the API using your credentials to unlock a much higher rate limit.
$ grip --user --pass
Or use a personal access token with an empty scope (note that a token is required if your GitHub account is set up with two-factor authentication):
$ grip --pass
You can persist these options in your local configuration. For security purposes, it's highly recommended that you use an access token over a password. (You could also keep your password safe by configuring Grip to grab your password from a password manager.)
There's also a work-in-progress branch to provide offline rendering. Once this resembles GitHub more precisely, it'll be exposed in the CLI, and will ultimately be used as a seamless fallback engine for when the API can't be accessed.
Grip always accesses GitHub over HTTPS, so your README and credentials are protected.
Here's how others from the community are using Grip.
Want to share your own? Say hello @joeyespo or submit a pull request.
$ git clone https://github.com/YOUR_USERNAME/YOUR_REPOSITORY.wiki.git $ cd YOUR_REPOSITORY.wiki $ grip
By Joshua Gourneau.
$ cd YOUR_DIR $ export GRIPURL=$(pwd)
CACHE_DIRECTORYconfig variable:
$ echo "CACHE_DIRECTORY = '$(pwd)/assets'" >> ~/.grip/settings.py
$ for f in *.md; do grip --export $f --no-inline; done $ for f in *.html; do sed -i '' "s?$GRIPURL/??g" $f; done
You can optionally compress the set of HTML files to
docs.tgzwith:
$ tar -czvf docs.tgz `ls | grep [\.]html$` assets
Looking for a cross platform solution? Here's an equivalent Python script.
To customize Grip, create
~/.grip/settings.py, then add one or more of the following variables:
HOST: The host to use when not provided as a CLI argument,
localhostby default
PORT: The port to use when not provided as a CLI argument,
6419by default
DEBUG: Whether to use Flask's debugger when an error happens,
Falseby default
DEBUG_GRIP: Prints extended information when an error happens,
Falseby default
API_URL: Base URL for the github API, for example that of a Github Enterprise instance.
https://api.github.comby default
CACHE_DIRECTORY: The directory, relative to
~/.grip, to place cached assets (this gets run through the following filter:
CACHE_DIRECTORY.format(version=__version__)),
'cache-{version}'by default
AUTOREFRESH: Whether to automatically refresh the Readme content when the file changes,
Trueby default
QUIET: Do not print extended information,
Falseby default
STYLE_URLS: Additional URLs that will be added to the rendered page,
[]by default
USERNAME: The username to use when not provided as a CLI argument,
Noneby default
PASSWORD: The password or personal access token to use when not provided as a CLI argument (Please don't save your passwords here. Instead, use an access token or drop in this code grab your password from a password manager),
Noneby default
Note that this is a Python file. If you see
'X' is not definederrors, you may have overlooked some quotes. For example:
USERNAME = 'your-username' PASSWORD = 'your-personal-access-token'
GRIPHOME: Specify an alternative
settings.pylocation,
~/.gripby default
GRIPURL: The URL of the Grip server,
/__/gripby default
This file is a normal Python script, so you can add more advanced configuration.
For example, to read a setting from the environment and provide a default value when it's not set:
PORT = os.environ.get('GRIP_PORT', 8080)
You can access the API directly with Python, using it in your own projects:
from grip import serveserve(port=8080)
Run main directly:
from grip import mainmain(argv=['-b', '8080'])
Or access the underlying Flask application for even more flexibility:
from grip import create_appgrip_app = create_app(user_content=True)
Use in your own app
Runs a local server and renders the Readme file located at
pathwhen visited in the browser.
serve(path=None, host=None, port=None, user_content=False, context=None, username=None, password=None, render_offline=False, render_wide=False, render_inline=False, api_url=None, title=None, autorefresh=True, browser=False, grip_class=None)
path: The filename to render, or the directory containing your Readme file, defaulting to the current working directory
host: The host to listen on, defaulting to the HOST configuration variable
port: The port to listen on, defaulting to the PORT configuration variable
user_content: Whether to render a document as user-content like user comments or issues
context: The project context to use when
user_contentis true, which takes the form of
username/project
username: The user to authenticate with GitHub to extend the API limit
password: The password to authenticate with GitHub to extend the API limit
render_offline: Whether to render locally using Python-Markdown (Note: this is a work in progress)
render_wide: Whether to render a wide page,
Falseby default (this has no effect when used with
user_content)
render_inline: Whether to inline the styles within the HTML file
api_url: A different base URL for the github API, for example that of a Github Enterprise instance. The default is the public API https://api.github.com.
title: The page title, derived from
pathby default
autorefresh: Automatically update the rendered content when the Readme file changes,
Trueby default
browser: Open a tab in the browser after the server starts.,
Falseby default
grip_class: Use a custom Grip class
Writes the specified Readme file to an HTML file with styles and assets inlined.
export(path=None, user_content=False, context=None, username=None, password=None, render_offline=False, render_wide=False, render_inline=True, out_filename=None, api_url=None, title=None, quiet=None, grip_class=None)
path: The filename to render, or the directory containing your Readme file, defaulting to the current working directory
user_content: Whether to render a document as user-content like user comments or issues
context: The project context to use when
user_contentis true, which takes the form of
username/project
username: The user to authenticate with GitHub to extend the API limit
password: The password to authenticate with GitHub to extend the API limit
render_offline: Whether to render locally using Python-Markdown (Note: this is a work in progress)
render_wide: Whether to render a wide page,
Falseby default (this has no effect when used with
user_content)
render_inline: Whether to inline the styles within the HTML file (Note: unlike the other API functions, this defaults to
True)
out_filename: The filename to write to,
.htmlby default
api_url: A different base URL for the github API, for example that of a Github Enterprise instance. The default is the public API https://api.github.com.
title: The page title, derived from
pathby default
quiet: Do not print to the terminal
grip_class: Use a custom Grip class
Creates a Flask application you can use to render and serve the Readme files. This is the same app used by
serveand
exportand initializes the cache, using the cached styles when available.
create_app(path=None, user_content=False, context=None, username=None, password=None, render_offline=False, render_wide=False, render_inline=False, api_url=None, title=None, text=None, grip_class=None)
path: The filename to render, or the directory containing your Readme file, defaulting to the current working directory
user_content: Whether to render a document as user-content like user comments or issues
context: The project context to use when
user_contentis true, which takes the form of
username/project
username: The user to authenticate with GitHub to extend the API limit
password: The password to authenticate with GitHub to extend the API limit
render_offline: Whether to render locally using Python-Markdown (Note: this is a work in progress)
render_wide: Whether to render a wide page,
Falseby default (this has no effect when used with
user_content)
render_inline: Whether to inline the styles within the HTML file
api_url: A different base URL for the github API, for example that of a Github Enterprise instance. The default is the public API https://api.github.com.
title: The page title, derived from
pathby default
text: A string or stream of Markdown text to render instead of being loaded from
path(Note:
pathcan be used to set the page title)
grip_class: Use a custom Grip class
Renders the application created by
create_appand returns the HTML that would normally appear when visiting that route.
render_app(app, route='/')
app: The Flask application to render
route: The route to render, '/' by default
Renders the specified markdown text without caching.
render_content(text, user_content=False, context=None, username=None, password=None, render_offline=False, api_url=None, title=None)
text: The Markdown text to render
user_content: Whether to render a document as user-content like user comments or issues
context: The project context to use when
user_contentis true, which takes the form of
username/project
username: The user to authenticate with GitHub to extend the API limit
password: The password to authenticate with GitHub to extend the API limit
render_offline: Whether to render locally using Python-Markdown (Note: this is a work in progress)
api_url: A different base URL for the github API, for example that of a Github Enterprise instance. This is required when not using the offline renderer.
title: The page title, derived from
pathby default
Renders the markdown from the specified path or text, without caching, and returns an HTML page that resembles the GitHub Readme view.
render_page(path=None, user_content=False, context=None, username=None, password=None, render_offline=False, render_wide=False, render_inline=False, api_url=None, title=None, text=None, quiet=None, grip_class=None)
path: The path to use for the page title, rendering
'README.md'if None
user_content: Whether to render a document as user-content like user comments or issues
context: The project context to use when
user_contentis true, which takes the form of
username/project
username: The user to authenticate with GitHub to extend the API limit
password: The password to authenticate with GitHub to extend the API limit
render_offline: Whether to render offline using Python-Markdown (Note: this is a work in progress)
render_wide: Whether to render a wide page,
Falseby default (this has no effect when used with
user_content)
render_inline: Whether to inline the styles within the HTML file
api_url: A different base URL for the github API, for example that of a Github Enterprise instance. The default is the public API https://api.github.com.
title: The page title, derived from
pathby default
text: A string or stream of Markdown text to render instead of being loaded from
path(Note:
pathcan be used to set the page title)
quiet: Do not print to the terminal
grip_class: Use a custom Grip class
Clears the cached styles and assets.
clear_cache(grip_class=None)
Runs Grip with the specified arguments.
main(argv=None, force_utf8=True)
argv: The arguments to run with,
sys.argv[1:]by default
force_utf8: Sets the default encoding to
utf-8in the current Python instance. This has no effect on Python 3 since Unicode is handled by default
A Flask application that can serve a file or directory containing a README.
Grip(source=None, auth=None, renderer=None, assets=None, render_wide=None, render_inline=None, title=None, autorefresh=None, quiet=None, grip_url=None, static_url_path=None, instance_path=None, **kwargs)
Returns the default renderer using the current config. This is only used if renderer is set to None in the constructor.
Grip.default_renderer()
Returns the default asset manager using the current config. This is only used if asset_manager is set to None in the constructor.
Grip.default_asset_manager()
Adds the application/x-font-woff and application/octet-stream content types if they are missing. Override to add additional content types on initialization.
Grip.add_content_types()
Clears the downloaded assets.
Grip.clear_cache()
Renders the application and returns the HTML unicode that would normally appear when visiting in the browser.
Grip.render(route=None)
route: The route to render,
/by default
Starts a server to render the README. This calls Flask.run internally.
Grip.run(host=None, port=None, debug=None, use_reloader=None, open_browser=False)
host: The hostname to listen on. Set this to
'0.0.0.0'to have the server available externally as well,
'localhost'by default
port: The port of the webserver. Defaults to
6419
debug: If given, enable or disable debug mode. See Flask.debug.
use_reloader: Should the server automatically restart the python process if modules were changed?
Falseby default unless the
DEBUG_GRIPsetting is specified.
open_browser: Opens the browser to the address when the server starts
Raised when
Grip.runis called while the server is already running.
AlreadyRunningError()
Raised when the specified Readme could not be found.
ReadmeNotFoundError(path=None, message=None)
Manages the style and font assets rendered with Readme pages. This is an abstract base class.
ReadmeAssetManager(cache_path, style_urls=None)
Manages the style and font assets rendered with Readme pages. Set cache_path to None to disable caching.
Reads Readme content from a URL subpath. This is an abstract base class.
ReadmeReader()
Reads Readme files from URL subpaths.
DirectoryReader(path=None, silent=False)
Reads Readme content from the provided unicode string.
TextReader(text, display_filename=None)
Reads Readme text from STDIN.
StdinReader(display_filename=None)
Renders the Readme. This is an abstract base class.
ReadmeRenderer(user_content=None, context=None)
Renders the specified Readme using the GitHub Markdown API.
GitHubRenderer(user_content=None, context=None, api_url=None, raw=None)
Renders the specified Readme locally using pure Python. Note: This is currently an incomplete feature.
OfflineRenderer(user_content=None, context=None)
The common Markdown file titles on GitHub.
SUPPORTED_TITLES = ['README', 'Home']
filename: The UTF-8 file to read.
The supported extensions, as defined by GitHub.
SUPPORTED_EXTENSIONS = ['.md', '.markdown']
This constant contains the names Grip looks for when no file is provided.
DEFAULT_FILENAMES = [title + ext for title in SUPPORTED_TITLES for ext in SUPPORTED_EXTENSIONS]
This constant contains the default Readme filename, namely:
DEFAULT_FILENAME = DEFAULT_FILENAMES[0] # README.md
This constant points to the default value if the
GRIPHOMEenvironment variable is not specified.
DEFAULT_GRIPHOME = '~/.grip'
The default URL of the Grip server and all its assets:
DEFAULT_GRIPURL = '/__/grip'
The default app_url value:
DEFAULT_API_URL = 'https://api.github.com'
Install the package and test requirements:
$ pip install -e .[tests]
Run tests with pytest:
$ pytest
Or to re-run tests as you make changes, use pytest-watch:
$ ptw
If you're experiencing a problem with Grip, it's likely that an assumption made about the GitHub API has been broken. To verify this, run:
$ pytest -m assumption
Since the external assumptions rely on an internet connection, you may want to skip them when developing locally. Tighten the cycle even further by stopping on the first failure with
-x:
$ pytest -xm "not assumption"
Or with pytest-watch:
$ ptw -- -xm "not assumption"
If your PR has been waiting a while, feel free to ping me on Twitter.