├── MANIFEST.in ├── nbextension ├── src │ ├── index.css │ ├── embed.js │ ├── index.js │ ├── extension.js │ └── renderer.js ├── README.md ├── package.json └── webpack.config.js ├── component ├── index.js ├── README.md ├── package.json ├── index.css ├── vanilla-table.js ├── fixed-data-table.js ├── virtualized-grid.js └── virtualized-table.js ├── .gitignore ├── .cookiecutter.yaml ├── jupyterlab_table ├── README.md └── __init__.py ├── uninstall.sh ├── labextension ├── README.md ├── src │ ├── index.css │ ├── output.js │ ├── plugin.js │ └── doc.js ├── package.json └── build_extension.js ├── RELEASE.md ├── setup.py ├── install.sh ├── README.md └── setupbase.py /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft jupyterlab_table/static 2 | graft labextension/src 3 | graft nbextension/src 4 | include labextension/package.json 5 | include nbextension/package.json 6 | -------------------------------------------------------------------------------- /nbextension/src/index.css: -------------------------------------------------------------------------------- 1 | div.output_subarea.output_JSONTable { 2 | max-width: 100%; 3 | padding: 0; 4 | padding-top: 0.4em; 5 | font-size: 14px; 6 | overflow: hidden; 7 | } 8 | -------------------------------------------------------------------------------- /component/index.js: -------------------------------------------------------------------------------- 1 | export VirtualizedTable from './virtualized-table'; 2 | export VirtualizedGrid from './virtualized-grid'; 3 | export FixedDataTable from './fixed-data-table'; 4 | export VanillaTable from './vanilla-table'; 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | labextension/lib/ 2 | nbextension/lib/ 3 | nbextension/embed/ 4 | node_modules/ 5 | npm-debug.log 6 | *.egg-info/ 7 | __pycache__/ 8 | build/ 9 | dist/ 10 | 11 | # Compiled javascript 12 | jupyterlab_table/static/ 13 | 14 | # OS X 15 | .DS_Store 16 | -------------------------------------------------------------------------------- /.cookiecutter.yaml: -------------------------------------------------------------------------------- 1 | default_context: 2 | author_name: "Grant Nestor" 3 | author_email: "grantnestor@gmail.com" 4 | mime_type: "application/vnd.dataresource+json" 5 | file_extension: "table.json" 6 | mime_short_name: "JSONTable" 7 | extension_name: "jupyterlab_table" 8 | -------------------------------------------------------------------------------- /component/README.md: -------------------------------------------------------------------------------- 1 | # component 2 | 3 | A React component for rendering JSONTable 4 | 5 | ## Structure 6 | 7 | * `index.js`: Entry point for React component(s) 8 | * `index.css`: Optional CSS file for styling React component(s) 9 | * `package.json`: Node package configuration, use this to add npm depedencies 10 | -------------------------------------------------------------------------------- /nbextension/src/embed.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Entry point for the unpkg bundle containing custom model definitions. 3 | * 4 | * It differs from the notebook bundle in that it does not need to define a 5 | * dynamic baseURL for the static assets and may load some css that would 6 | * already be loaded by the notebook otherwise. 7 | */ 8 | 9 | export { version } from '../package.json'; 10 | -------------------------------------------------------------------------------- /jupyterlab_table/README.md: -------------------------------------------------------------------------------- 1 | # jupyterlab_table 2 | 3 | Single Python package for lab and notebook extensions 4 | 5 | ## Structure 6 | 7 | * `static`: Built Javascript from `../labextension/` and `../nbextension/` 8 | * `__init__.py`: Exports paths and metadata of lab and notebook extensions and exports an optional `display` method that can be imported into a notebook and used to easily display data using this renderer 9 | -------------------------------------------------------------------------------- /component/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jupyterlab_table_react", 3 | "version": "1.0.0", 4 | "description": "A React component for rendering JSON schema table", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "jupyter", 11 | "react" 12 | ], 13 | "author": "Grant Nestor", 14 | "license": "ISC", 15 | "dependencies": { 16 | "fixed-data-table": "^0.6.3", 17 | "jsontableschema": "^0.2.2", 18 | "react": "^15.3.2", 19 | "react-addons-shallow-compare": "^15.4.2", 20 | "react-virtualized": "^8.11.4" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | nbExtFlags=$1 3 | 4 | jupyter lab --version 5 | if [ $? -eq 0 ]; then 6 | jupyter labextension uninstall --py $nbExtFlags jupyterlab_test 7 | fi 8 | 9 | jupyter notebook --version 10 | if [ $? -eq 0 ]; then 11 | jupyter nbextension uninstall --py $nbExtFlags jupyterlab_test 12 | fi 13 | 14 | pip --version 15 | if [ $? -eq 0 ]; then 16 | pip uninstall -v . 17 | else 18 | echo "'pip --version' failed, therefore pip is not installed. In order to perform 19 | an install of jupyterlab_table you must have both pip and npm installed on 20 | your machine! See https://packaging.python.org/installing/ for installation instructions." 21 | fi 22 | -------------------------------------------------------------------------------- /labextension/README.md: -------------------------------------------------------------------------------- 1 | # labextension 2 | 3 | A JupyterLab extension for rendering JSON Table Schema 4 | 5 | ## Prerequisites 6 | 7 | * `jupyterlab@^0.18.0` 8 | 9 | ## Development 10 | 11 | Install dependencies and build Javascript: 12 | 13 | ```bash 14 | npm install 15 | ``` 16 | 17 | Re-build Javascript: 18 | 19 | ```bash 20 | npm run build 21 | ``` 22 | 23 | Watch `/src` directory and re-build on changes: 24 | 25 | ```bash 26 | npm run watch 27 | ``` 28 | 29 | Manage extension 30 | 31 | ```bash 32 | # Install 33 | npm run extension:install 34 | # Enable 35 | npm run extension:enable 36 | # Disable 37 | npm run extension:disable 38 | # Uninstall 39 | npm run extension:uninstall 40 | ``` 41 | -------------------------------------------------------------------------------- /nbextension/src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Entry point for the notebook bundle containing custom model definitions. 3 | * Setup notebook base URL 4 | * Some static assets may be required by the custom widget javascript. The base 5 | * url for the notebook is not known at build time and is therefore computed 6 | * dynamically. 7 | */ 8 | 9 | __webpack_public_path__ = document 10 | .querySelector('body') 11 | .getAttribute('data-base-url') + 12 | 'nbextensions/jupyterlab_table/'; 13 | 14 | /** 15 | * Export widget models and views, and the npm package version number. 16 | */ 17 | export { register_renderer, render_cells } from './renderer.js'; 18 | export { version } from '../package.json'; 19 | -------------------------------------------------------------------------------- /nbextension/README.md: -------------------------------------------------------------------------------- 1 | # nbextension 2 | 3 | A Jupyter Notebook extension for rendering JSON Table Schema 4 | 5 | ## Prerequisites 6 | 7 | * `notebook@>=4.3.0` 8 | 9 | ## Development 10 | 11 | Install dependencies and build Javascript: 12 | 13 | ```bash 14 | npm install 15 | ``` 16 | 17 | Re-build Javascript: 18 | 19 | ```bash 20 | npm run build 21 | ``` 22 | 23 | Watch `/src` directory and re-build on changes: 24 | 25 | ```bash 26 | npm run watch 27 | ``` 28 | 29 | Manage extension 30 | 31 | ```bash 32 | # Install 33 | npm run extension:install 34 | # Enable 35 | npm run extension:enable 36 | # Disable 37 | npm run extension:disable 38 | # Uninstall 39 | npm run extension:uninstall 40 | ``` 41 | -------------------------------------------------------------------------------- /labextension/src/index.css: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) Jupyter Development Team. 3 | Distributed under the terms of the Modified BSD License. 4 | */ 5 | 6 | .jp-OutputWidgetJSONTable, .jp-DocWidgetJSONTable { 7 | width: 100%; 8 | padding: 0; 9 | font-size: 13px; 10 | } 11 | 12 | .jp-OutputWidgetJSONTable { 13 | padding-top: 5px; 14 | height: 360px; 15 | } 16 | 17 | .jp-DocWidgetJSONTable { 18 | overflow: hidden; 19 | } 20 | 21 | .jp-DocWidgetJSONTable .jp-mod-error { 22 | width: 100%; 23 | min-height: 100%; 24 | text-align: center; 25 | padding: 10px; 26 | box-sizing: border-box; 27 | } 28 | 29 | .jp-DocWidgetJSONTable .jp-mod-error h2 { 30 | font-size: 18px; 31 | font-weight: 500; 32 | padding-bottom: 10px; 33 | } 34 | 35 | .jp-DocWidgetJSONTable .jp-mod-error pre { 36 | text-align: left; 37 | padding: 10px; 38 | overflow: hidden; 39 | } 40 | -------------------------------------------------------------------------------- /nbextension/src/extension.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains the javascript that is run when the notebook is loaded. 3 | * It contains some requirejs configuration and the `load_ipython_extension` 4 | * which is required for any notebook extension. 5 | */ 6 | 7 | /** 8 | * Configure requirejs. 9 | */ 10 | if (window.require) { 11 | window.require.config({ 12 | map: { 13 | '*': { 14 | 'jupyterlab_table': 'nbextensions/jupyterlab_table/index' 15 | } 16 | } 17 | }); 18 | } 19 | 20 | /** 21 | * Export the required load_ipython_extention. 22 | */ 23 | export function load_ipython_extension() { 24 | define( 25 | [ 26 | 'nbextensions/jupyterlab_table/index', 27 | 'base/js/namespace', 28 | 'base/js/events', 29 | 'notebook/js/outputarea' 30 | ], 31 | (Extension, Jupyter, events, outputarea) => { 32 | const { notebook } = Jupyter; 33 | const { OutputArea } = outputarea; 34 | Extension.register_renderer(notebook, events, OutputArea); 35 | Extension.render_cells(notebook); 36 | } 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /component/index.css: -------------------------------------------------------------------------------- 1 | .fixedDataTableLayout_main { 2 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 3 | font-size: 14px; 4 | } 5 | 6 | .public_fixedDataTableCell_main { 7 | border-color: #ddd; 8 | } 9 | 10 | .public_fixedDataTable_header { 11 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 12 | font-size: 14px; 13 | } 14 | 15 | .public_fixedDataTable_header { 16 | background: #fff; 17 | } 18 | 19 | .public_fixedDataTable_header, .public_fixedDataTable_header .public_fixedDataTableCell_main { 20 | background: #fff; 21 | text-align: center; 22 | } 23 | 24 | .public_fixedDataTableCell_cellContent { 25 | padding: 6px 13px; 26 | } 27 | 28 | .public_fixedDataTableRow_highlighted, .public_fixedDataTableRow_highlighted .public_fixedDataTableCell_main { 29 | background: #f8f8f8; 30 | } 31 | 32 | .dataframe { 33 | width: 100%; 34 | border-spacing: 1px; 35 | } 36 | 37 | .dataframe .header { 38 | text-align: right; 39 | } 40 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Making a jupyterlab_table release 2 | 3 | This document guides an extension maintainer through creating and publishing a release of jupyterlab_table. This process creates a Python source package and a Python universal wheel and uploads them to PyPI. 4 | 5 | ## Update version number 6 | 7 | Update the version number in `setup.py`, `labextension/package.json`, and `nbextension/package.json`. 8 | 9 | Commit your changes, add git tag for this version, and push both commit and tag to your origin/remote repo. 10 | 11 | ## Remove generated files 12 | 13 | Remove old Javascript bundle and Python package builds: 14 | 15 | ```bash 16 | git clean -xfd 17 | ``` 18 | 19 | ## Build the package 20 | 21 | Build the Javascript extension bundle, then build the Python package and wheel: 22 | 23 | ```bash 24 | bash build.js 25 | python setup.py sdist 26 | python setup.py bdist_wheel --universal 27 | ``` 28 | 29 | ## Upload the package 30 | 31 | Upload the Python package and wheel with [twine](https://github.com/pypa/twine). See the Python documentation on [package uploading](https://packaging.python.org/distributing/#uploading-your-project-to-pypi) 32 | for [twine](https://github.com/pypa/twine) setup instructions and for why twine is the recommended uploading method. 33 | 34 | ```bash 35 | twine upload dist/* 36 | ``` 37 | -------------------------------------------------------------------------------- /component/vanilla-table.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | // hack: `stream.Transform` (stream-browserify) is undefined in `csv-parse` when 4 | // built with @jupyterlabextension-builder 5 | import infer from 'jsontableschema/lib/infer'; 6 | // import { infer } from 'jsontableschema'; 7 | import './index.css'; 8 | 9 | function inferSchema(data) { 10 | const headers = data.reduce((result, row) => [...new Set([...result, ...Object.keys(row)])], []); 11 | const values = data.map(row => Object.values(row)); 12 | return infer(headers, values); 13 | } 14 | 15 | export default class VanillaTable extends React.Component { 16 | 17 | render() { 18 | let { schema, data } = this.props; 19 | if (!schema) schema = inferSchema(data); 20 | return ( 21 | 22 | 23 | 24 | { 25 | schema.fields.map((field, index) => ( 26 | 27 | )) 28 | } 29 | 30 | 31 | 32 | { 33 | props.data.map((row, rowIndex) => 34 | 35 | { 36 | schema.fields.map((field, index) => ( 37 | 38 | )) 39 | } 40 | 41 | ) 42 | } 43 | 44 |
{field.name}
{row[field.name]}
45 | ); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /nbextension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "jupyterlab_table_nbextension", 4 | "version": "0.18.0", 5 | "description": "A Jupyter Notebook extension for rendering JSON Table Schema", 6 | "author": "Grant Nestor ", 7 | "main": "src/index.js", 8 | "keywords": [ 9 | "jupyter", 10 | "jupyterlab", 11 | "jupyterlab extension" 12 | ], 13 | "scripts": { 14 | "build": "webpack", 15 | "watch": "watch \"npm install\" src ../component --wait 10 --ignoreDotFiles", 16 | "preinstall": "npm install ../component", 17 | "prepublish": "npm run build", 18 | "extension:install": "jupyter nbextension install --symlink --py --sys-prefix jupyterlab_table", 19 | "extension:uninstall": "jupyter nbextension uninstall --py --sys-prefix jupyterlab_table", 20 | "extension:enable": "jupyter nbextension enable --py --sys-prefix jupyterlab_table", 21 | "extension:disable": "jupyter nbextension disable --py --sys-prefix jupyterlab_table" 22 | }, 23 | "dependencies": { 24 | "react": "^15.3.2", 25 | "react-dom": "^15.3.2" 26 | }, 27 | "devDependencies": { 28 | "babel-core": "^6.18.2", 29 | "babel-loader": "^6.4.0", 30 | "babel-preset-latest": "^6.16.0", 31 | "babel-preset-react": "^6.16.0", 32 | "babel-preset-stage-0": "^6.22.0", 33 | "css-loader": "^0.25.0", 34 | "file-loader": "^0.9.0", 35 | "json-loader": "^0.5.4", 36 | "style-loader": "^0.13.1", 37 | "url-loader": "^0.5.7", 38 | "watch": "^1.0.1", 39 | "webpack": "^2.2.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /labextension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "jupyterlab_table_labextension", 4 | "version": "0.18.0", 5 | "description": "A JupyterLab extension for rendering JSON Table Schema", 6 | "author": "Grant Nestor ", 7 | "main": "src/plugin.js", 8 | "keywords": [ 9 | "jupyter", 10 | "jupyterlab", 11 | "jupyterlab extension" 12 | ], 13 | "scripts": { 14 | "build": "node build_extension.js", 15 | "watch": "watch \"npm install\" src ../component --wait 10 --ignoreDotFiles", 16 | "preinstall": "npm install ../component", 17 | "prepublish": "npm run build", 18 | "extension:install": "jupyter labextension install --symlink --py --sys-prefix jupyterlab_table", 19 | "extension:uninstall": "jupyter labextension uninstall --py --sys-prefix jupyterlab_table", 20 | "extension:enable": "jupyter labextension enable --py --sys-prefix jupyterlab_table", 21 | "extension:disable": "jupyter labextension disable --py --sys-prefix jupyterlab_table" 22 | }, 23 | "dependencies": { 24 | "@jupyterlab/apputils": "^0.1.3", 25 | "@jupyterlab/codemirror": "^0.1.3", 26 | "@jupyterlab/docregistry": "^0.1.3", 27 | "@jupyterlab/rendermime": "^0.1.3", 28 | "@phosphor/algorithm": "^0.1.1", 29 | "@phosphor/virtualdom": "^0.1.1", 30 | "@phosphor/widgets": "^0.3.0", 31 | "react": "^15.3.2", 32 | "react-dom": "^15.3.2" 33 | }, 34 | "devDependencies": { 35 | "@jupyterlab/extension-builder": "^0.10.0", 36 | "babel-core": "^6.18.2", 37 | "babel-loader": "^6.2.7", 38 | "babel-preset-latest": "^6.16.0", 39 | "babel-preset-react": "^6.16.0", 40 | "babel-preset-stage-0": "^6.16.0", 41 | "watch": "^1.0.1" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /labextension/build_extension.js: -------------------------------------------------------------------------------- 1 | var buildExtension = require('@jupyterlab/extension-builder').buildExtension; 2 | var path = require('path'); 3 | 4 | buildExtension({ 5 | name: 'jupyterlab_table', 6 | entry: path.join(__dirname, 'src', 'plugin.js'), 7 | outputDir: path.join( 8 | __dirname, 9 | '..', 10 | 'jupyterlab_table', 11 | 'static' 12 | ), 13 | useDefaultLoaders: false, 14 | config: { 15 | module: { 16 | loaders: [ 17 | { test: /\.html$/, loader: 'file-loader' }, 18 | { test: /\.(jpg|png|gif)$/, loader: 'file-loader' }, 19 | { 20 | test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, 21 | loader: 'url-loader?limit=10000&mimetype=application/font-woff' 22 | }, 23 | { 24 | test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, 25 | loader: 'url-loader?limit=10000&mimetype=application/font-woff' 26 | }, 27 | { 28 | test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, 29 | loader: 'url-loader?limit=10000&mimetype=application/octet-stream' 30 | }, 31 | { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: 'file-loader' }, 32 | { 33 | test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, 34 | loader: 'url-loader?limit=10000&mimetype=image/svg+xml' 35 | }, 36 | { test: /\.json$/, loader: 'json-loader' }, 37 | { 38 | test: /\.js$/, 39 | include: [ 40 | path.join(__dirname, 'src'), 41 | path.join( 42 | __dirname, 43 | 'node_modules', 44 | 'jupyterlab_table_react' 45 | ) 46 | ], 47 | loader: 'babel-loader', 48 | query: { presets: ['latest', 'stage-0', 'react'] } 49 | } 50 | ] 51 | } 52 | } 53 | }); 54 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | from setupbase import create_cmdclass, install_npm 3 | 4 | cmdclass = create_cmdclass(['labextension', 'nbextension']) 5 | cmdclass['labextension'] = install_npm('labextension') 6 | cmdclass['nbextension'] = install_npm('nbextension') 7 | 8 | setup_args = dict( 9 | name = 'jupyterlab_table', 10 | version = '0.18.0', 11 | packages = ['jupyterlab_table'], 12 | author = 'Grant Nestor', 13 | author_email = 'grantnestor@gmail.com', 14 | url = 'http://jupyter.org', 15 | license = 'BSD', 16 | platforms = "Linux, Mac OS X, Windows", 17 | keywords = [ 18 | 'ipython', 19 | 'jupyter', 20 | 'jupyterlab', 21 | 'extension', 22 | 'renderer', 23 | 'pandas', 24 | 'frictionlessdata', 25 | 'tableschema', 26 | 'dataresource' 27 | ], 28 | classifiers = [ 29 | 'Intended Audience :: Developers', 30 | 'Intended Audience :: System Administrators', 31 | 'Intended Audience :: Science/Research', 32 | 'License :: OSI Approved :: BSD License', 33 | 'Programming Language :: Python', 34 | 'Programming Language :: Python :: 2.7', 35 | 'Programming Language :: Python :: 3', 36 | 'Programming Language :: Python :: 3.3', 37 | 'Programming Language :: Python :: 3.4', 38 | 'Programming Language :: Python :: 3.5', 39 | ], 40 | cmdclass = cmdclass, 41 | install_requires = [ 42 | 'jupyterlab>=0.18.0', 43 | 'notebook>=4.3.0', 44 | 'ipython>=1.0.0' 45 | ] 46 | ) 47 | 48 | if __name__ == '__main__': 49 | setup(**setup_args) 50 | -------------------------------------------------------------------------------- /component/fixed-data-table.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Table, 4 | Column, 5 | Cell 6 | } from 'fixed-data-table'; 7 | import 'fixed-data-table/dist/fixed-data-table.min.css'; 8 | // hack: `stream.Transform` (stream-browserify) is undefined in `csv-parse` when 9 | // built with @jupyterlabextension-builder 10 | import infer from 'jsontableschema/lib/infer'; 11 | // import { infer } from 'jsontableschema'; 12 | import './index.css'; 13 | 14 | const ROW_HEIGHT = 34; 15 | 16 | function inferSchema(data) { 17 | const headers = data.reduce((result, row) => [...new Set([...result, ...Object.keys(row)])], []); 18 | const values = data.map(row => Object.values(row)); 19 | return infer(headers, values); 20 | } 21 | 22 | export default class FixedDataTable extends React.Component { 23 | 24 | state = { 25 | columnWidths: {} 26 | } 27 | 28 | render() { 29 | let { schema, data } = this.props; 30 | if (!schema) schema = inferSchema(data); 31 | return ( 32 | { 39 | this.setState(({columnWidths}) => ({ 40 | columnWidths: { 41 | ...columnWidths, 42 | [columnKey]: columnWidth, 43 | } 44 | })); 45 | }} 46 | > 47 | { 48 | schema.fields.map((field, fieldIndex) => 49 | 54 | {field.name} 55 | } 56 | cell={props => 57 | {data[props.rowIndex][field.name]} 58 | } 59 | fixed={false} 60 | isResizable={true} 61 | /> 62 | ) 63 | } 64 |
65 | ); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | nbExtFlags=$1 4 | 5 | bash build.sh 6 | 7 | pip --version 8 | if [ $? -eq 0 ]; then 9 | echo pip is installed 10 | else 11 | echo "'pip --version' failed, therefore pip is not installed. In order to perform 12 | a developer install of jupyterlab_table you must have pip installed on 13 | your machine! See https://packaging.python.org/installing/ for installation instructions." 14 | exit 1 15 | fi 16 | 17 | pip install -v -e . 18 | 19 | jupyter lab --version 20 | if [ $? -eq 0 ]; then 21 | echo jupyter lab is installed 22 | if [[ "$OSTYPE" == "msys" ]]; then 23 | jupyter labextension install --py $nbExtFlags jupyterlab_table 24 | else 25 | jupyter labextension install --py --symlink $nbExtFlags jupyterlab_table 26 | fi 27 | jupyter labextension enable --py $nbExtFlags jupyterlab_table 28 | else 29 | echo "'jupyter lab --version' failed, therefore jupyter lab is not installed. In 30 | order to perform a developer install of jupyterlab_table you must 31 | have jupyter lab on your machine! Install using 'pip install jupyterlab' or 32 | follow instructions at https://github.com/jupyterlab/jupyterlab/blob/master/CONTRIBUTING.md#installing-jupyterlab for developer install." 33 | fi 34 | 35 | jupyter notebook --version 36 | if [ $? -eq 0 ]; then 37 | echo jupyter notebook is installed 38 | if [[ "$OSTYPE" == "msys" ]]; then 39 | jupyter nbextension install --py $nbExtFlags jupyterlab_table 40 | else 41 | jupyter nbextension install --py --symlink $nbExtFlags jupyterlab_table 42 | fi 43 | jupyter nbextension enable --py $nbExtFlags jupyterlab_table 44 | else 45 | echo "'jupyter notebook --version' failed, therefore jupyter notebook is not 46 | installed. In order to perform a developer install of 47 | jupyterlab_table you must have jupyter notebook on your machine! 48 | Install using 'pip install notebook' or follow instructions at 49 | https://github.com/jupyter/notebook/blob/master/CONTRIBUTING.rst#installing-the-jupyter-notebook 50 | for developer install." 51 | fi 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jupyterlab_table 2 | 3 | A JupyterLab and Jupyter Notebook extension for rendering [JSON Table Schema](http://frictionlessdata.io/guides/json-table-schema/) 4 | 5 | ![output renderer](http://g.recordit.co/X8XNLpKs21.gif) 6 | 7 | ## Prerequisites 8 | 9 | * JupyterLab ^0.18.0 and/or Notebook >=4.3.0 10 | 11 | ## Usage 12 | 13 | To render JSONTable output in IPython: 14 | 15 | ```python 16 | from jupyterlab_table import JSONTable 17 | 18 | JSONTable(data=[ 19 | { 20 | "Date": "2000-03-01", 21 | "Adj Close": 33.68, 22 | "Open": 89.62, 23 | "Low": 88.94, 24 | "Volume": 106889800, 25 | "High": 94.09, 26 | "Close": 90.81 27 | }, 28 | { 29 | "Date": "2000-03-02", 30 | "Adj Close": 34.63, 31 | "Open": 91.81, 32 | "Low": 91.12, 33 | "Volume": 106932600, 34 | "High": 95.37, 35 | "Close": 93.37 36 | } 37 | ], schema={ 38 | "fields": [ 39 | { 40 | "type": "any", 41 | "name": "Date" 42 | }, 43 | { 44 | "type": "number", 45 | "name": "Open" 46 | }, 47 | { 48 | "type": "number", 49 | "name": "High" 50 | }, 51 | { 52 | "type": "number", 53 | "name": "Low" 54 | }, 55 | { 56 | "type": "number", 57 | "name": "Close" 58 | }, 59 | { 60 | "type": "integer", 61 | "name": "Volume" 62 | }, 63 | { 64 | "type": "number", 65 | "name": "Adj Close" 66 | } 67 | ] 68 | }) 69 | ``` 70 | 71 | Using a pandas DataFrame: 72 | 73 | ```python 74 | from jupyterlab_table import JSONTable 75 | import pandas 76 | import numpy 77 | 78 | df = pandas.DataFrame(numpy.random.randn(2, 2)) 79 | JSONTable(df) 80 | ``` 81 | 82 | To render a .table.json file as a tree, simply open it: 83 | 84 | ![file renderer](http://g.recordit.co/3Lbf119uA1.gif) 85 | 86 | ## Install 87 | 88 | ```bash 89 | pip install jupyterlab_table 90 | # For JupyterLab 91 | jupyter labextension install --symlink --py --sys-prefix jupyterlab_table 92 | jupyter labextension enable --py --sys-prefix jupyterlab_table 93 | # For Notebook 94 | jupyter nbextension install --symlink --py --sys-prefix jupyterlab_table 95 | jupyter nbextension enable --py --sys-prefix jupyterlab_table 96 | ``` 97 | 98 | ## Development 99 | 100 | ```bash 101 | pip install -e . 102 | # For JupyterLab 103 | jupyter labextension install --symlink --py --sys-prefix jupyterlab_table 104 | jupyter labextension enable --py --sys-prefix jupyterlab_table 105 | # For Notebook 106 | jupyter nbextension install --symlink --py --sys-prefix jupyterlab_table 107 | jupyter nbextension enable --py --sys-prefix jupyterlab_table 108 | ``` 109 | -------------------------------------------------------------------------------- /labextension/src/output.js: -------------------------------------------------------------------------------- 1 | import { Widget } from '@phosphor/widgets'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import ReactDOMServer from 'react-dom/server'; 5 | import { VirtualizedGrid, VirtualizedTable } from 'jupyterlab_table_react'; 6 | 7 | const MIME_TYPE = 'application/vnd.dataresource+json'; 8 | const CLASS_NAME = 'jp-OutputWidgetJSONTable'; 9 | 10 | /** 11 | * A Phosphor widget for rendering JSONTable 12 | */ 13 | export class OutputWidget extends Widget { 14 | constructor(options) { 15 | super(); 16 | this._mimeType = options.mimeType; 17 | this._data = options.model.data; 18 | this._metadata = options.model.metadata; 19 | this.addClass(CLASS_NAME); 20 | } 21 | 22 | /** 23 | * A message handler invoked on an `'after-attach'` message 24 | */ 25 | onAfterAttach(msg) { 26 | /* Render initial data */ 27 | this._render(); 28 | } 29 | 30 | /** 31 | * A message handler invoked on an `'before-detach'` message 32 | */ 33 | onBeforeDetach(msg) { 34 | /* Dispose of resources used by this widget */ 35 | ReactDOM.unmountComponentAtNode(this.node); 36 | } 37 | 38 | /** 39 | * A message handler invoked on a `'child-added'` message 40 | */ 41 | onChildAdded(msg) { 42 | /* e.g. Inject a static image representation into the mime bundle for 43 | * endering on Github, etc. 44 | */ 45 | // renderLibrary.toPng(this.node).then(url => { 46 | // const data = url.split(',')[1]; 47 | // this._data.set('image/png', data); 48 | // }) 49 | } 50 | 51 | /** 52 | * A message handler invoked on a `'resize'` message 53 | */ 54 | onResize(msg) { 55 | /* Re-render on resize */ 56 | this._render(); 57 | } 58 | 59 | /** 60 | * Render data to DOM node 61 | */ 62 | _render() { 63 | const { data, schema } = this._data.get(this._mimeType); 64 | const metadata = this._metadata.get(this._mimeType); 65 | const type = metadata && metadata.format && metadata.format === 'table' 66 | ? VirtualizedTable 67 | : VirtualizedGrid; 68 | const props = { 69 | data, 70 | schema, 71 | metadata, 72 | width: this.node.offsetWidth, 73 | height: 360, 74 | fontSize: 13 75 | }; 76 | ReactDOM.render(React.createElement(type, props), this.node); 77 | } 78 | } 79 | 80 | export class OutputRenderer { 81 | /** 82 | * The mime types that this OutputRenderer accepts 83 | */ 84 | mimeTypes = [MIME_TYPE]; 85 | 86 | /** 87 | * Whether the renderer can render given the render options 88 | */ 89 | canRender(options) { 90 | return this.mimeTypes.indexOf(options.mimeType) !== -1; 91 | } 92 | 93 | /** 94 | * Render the transformed mime bundle 95 | */ 96 | render(options) { 97 | return new OutputWidget(options); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /labextension/src/plugin.js: -------------------------------------------------------------------------------- 1 | import { IRenderMime } from '@jupyterlab/rendermime'; 2 | import { IDocumentRegistry } from '@jupyterlab/docregistry'; 3 | import { ILayoutRestorer, InstanceTracker } from '@jupyterlab/apputils'; 4 | import { toArray, ArrayExt } from '@phosphor/algorithm'; 5 | import { OutputRenderer } from './output'; 6 | import { DocWidgetFactory } from './doc'; 7 | import './index.css'; 8 | 9 | /** 10 | * The name of the factory 11 | */ 12 | const FACTORY = 'JSONTable'; 13 | 14 | /** 15 | * Set the extensions associated with application/vnd.dataresource+json 16 | */ 17 | const EXTENSIONS = ['.tableschema.json', '.dataresource.json']; 18 | const DEFAULT_EXTENSIONS = ['.tableschema.json', '.dataresource.json']; 19 | 20 | /** 21 | * Activate the extension. 22 | */ 23 | function activatePlugin(app, rendermime, registry, restorer) { 24 | /** 25 | * Calculate the index of the renderer in the array renderers 26 | * e.g. Insert this renderer after any renderers with mime type that matches 27 | * "+json" 28 | */ 29 | // const index = ArrayExt.findLastIndex( 30 | // toArray(rendermime.mimeTypes()), 31 | // mime => mime.endsWith('+json') 32 | // ) + 1; 33 | /* ...or just insert it at the top */ 34 | const index = 0; 35 | 36 | /** 37 | * Add output renderer for application/vnd.tableschema+json data 38 | */ 39 | rendermime.addRenderer( 40 | { 41 | mimeType: 'application/vnd.tableschema+json', 42 | renderer: new OutputRenderer() 43 | }, 44 | index 45 | ); 46 | 47 | /** 48 | * Add output renderer for application/vnd.dataresource+json data 49 | */ 50 | rendermime.addRenderer( 51 | { 52 | mimeType: 'application/vnd.dataresource+json', 53 | renderer: new OutputRenderer() 54 | }, 55 | index 56 | ); 57 | 58 | const factory = new DocWidgetFactory({ 59 | fileExtensions: EXTENSIONS, 60 | defaultFor: DEFAULT_EXTENSIONS, 61 | name: FACTORY 62 | }); 63 | 64 | /** 65 | * Add document renderer for .table.json files 66 | */ 67 | registry.addWidgetFactory(factory); 68 | 69 | const tracker = new InstanceTracker({ 70 | namespace: 'JSONTable', 71 | shell: app.shell 72 | }); 73 | 74 | /** 75 | * Handle widget state deserialization 76 | */ 77 | restorer.restore(tracker, { 78 | command: 'file-operations:open', 79 | args: widget => ({ path: widget.context.path, factory: FACTORY }), 80 | name: widget => widget.context.path 81 | }); 82 | 83 | /** 84 | * Serialize widget state 85 | */ 86 | factory.widgetCreated.connect((sender, widget) => { 87 | tracker.add(widget); 88 | /* Notify the instance tracker if restore data needs to update */ 89 | widget.context.pathChanged.connect(() => { 90 | tracker.save(widget); 91 | }); 92 | }); 93 | } 94 | 95 | /** 96 | * Configure jupyterlab plugin 97 | */ 98 | const Plugin = { 99 | id: 'jupyter.extensions.JSONTable', 100 | requires: [IRenderMime, IDocumentRegistry, ILayoutRestorer], 101 | activate: activatePlugin, 102 | autoStart: true 103 | }; 104 | 105 | export default Plugin; 106 | -------------------------------------------------------------------------------- /nbextension/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var version = require('./package.json').version; 3 | 4 | /** 5 | * Custom webpack loaders are generally the same for all webpack bundles, hence 6 | * stored in a separate local variable. 7 | */ 8 | var loaders = [ 9 | { 10 | test: /\.js$/, 11 | include: [ 12 | path.join(__dirname, 'src'), 13 | path.join(__dirname, 'node_modules', 'jupyterlab_table_react') 14 | ], 15 | loader: 'babel-loader', 16 | query: { presets: [ 'latest', 'stage-0', 'react' ] } 17 | }, 18 | { test: /\.json$/, loader: 'json-loader' }, 19 | { test: /\.css$/, loader: 'style-loader!css-loader' }, 20 | { test: /\.html$/, loader: 'file-loader' }, 21 | { test: /\.(jpg|png|gif)$/, loader: 'file-loader' }, 22 | { 23 | test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, 24 | loader: 'url-loader?limit=10000&mimetype=application/font-woff' 25 | }, 26 | { 27 | test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, 28 | loader: 'url-loader?limit=10000&mimetype=application/font-woff' 29 | }, 30 | { 31 | test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, 32 | loader: 'url-loader?limit=10000&mimetype=application/octet-stream' 33 | }, 34 | { test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: 'file-loader' }, 35 | { 36 | test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, 37 | loader: 'url-loader?limit=10000&mimetype=image/svg+xml' 38 | } 39 | ]; 40 | 41 | var base = { 42 | output: { 43 | libraryTarget: 'amd', 44 | devtoolModuleFilenameTemplate: 'webpack:///[absolute-resource-path]' 45 | }, 46 | devtool: 'source-map', 47 | module: { loaders }, 48 | externals: [ 49 | 'nbextensions/jupyterlab_table/index', 50 | 'base/js/namespace', 51 | 'base/js/events', 52 | 'notebook/js/outputarea' 53 | ] 54 | }; 55 | 56 | module.exports = [ 57 | /** 58 | * Notebook extension 59 | * 60 | * This bundle only contains the part of the JavaScript that is run on 61 | * load of the notebook. This section generally only performs 62 | * some configuration for requirejs, and provides the legacy 63 | * "load_ipython_extension" function which is required for any notebook 64 | * extension. 65 | */ 66 | Object.assign({}, base, { 67 | entry: path.join(__dirname, 'src', 'extension.js'), 68 | output: Object.assign({}, base.output, { 69 | filename: 'extension.js', 70 | path: path.join( 71 | __dirname, 72 | '..', 73 | 'jupyterlab_table', 74 | 'static' 75 | ) 76 | }) 77 | }), 78 | /** 79 | * Bundle for the notebook containing the custom widget views and models 80 | * 81 | * This bundle contains the implementation for the custom widget views and 82 | * custom widget. 83 | * 84 | * It must be an amd module 85 | */ 86 | Object.assign({}, base, { 87 | entry: path.join(__dirname, 'src', 'index.js'), 88 | output: Object.assign({}, base.output, { 89 | filename: 'index.js', 90 | path: path.join( 91 | __dirname, 92 | '..', 93 | 'jupyterlab_table', 94 | 'static' 95 | ) 96 | }) 97 | }), 98 | /** 99 | * Embeddable jupyterlab_table bundle 100 | * 101 | * This bundle is generally almost identical to the notebook bundle 102 | * containing the custom widget views and models. 103 | * 104 | * The only difference is in the configuration of the webpack public path 105 | * for the static assets. 106 | * 107 | * It will be automatically distributed by unpkg to work with the static 108 | * widget embedder. 109 | * 110 | * The target bundle is always `lib/index.js`, which is the path required 111 | * by the custom widget embedder. 112 | */ 113 | Object.assign({}, base, { 114 | entry: './src/embed.js', 115 | output: Object.assign({}, base.output, { 116 | filename: 'index.js', 117 | path: path.join(__dirname, 'embed'), 118 | publicPath: 'https://unpkg.com/jupyterlab_table@' + 119 | version + 120 | '/lib/' 121 | }) 122 | }) 123 | ]; 124 | -------------------------------------------------------------------------------- /labextension/src/doc.js: -------------------------------------------------------------------------------- 1 | import { Widget } from '@phosphor/widgets'; 2 | import { ABCWidgetFactory } from '@jupyterlab/docregistry'; 3 | import { ActivityMonitor } from '@jupyterlab/coreutils'; 4 | import { runMode } from '@jupyterlab/codemirror'; 5 | import React from 'react'; 6 | import ReactDOM from 'react-dom'; 7 | import { VirtualizedGrid, VirtualizedTable } from 'jupyterlab_table_react'; 8 | 9 | const CLASS_NAME = 'jp-DocWidgetJSONTable'; 10 | const RENDER_TIMEOUT = 1000; 11 | 12 | /** 13 | * A component for rendering error messages 14 | */ 15 | class ErrorDisplay extends React.Component { 16 | componentDidUpdate() { 17 | runMode(this.props.content, { name: 'javascript', json: true }, this.ref); 18 | } 19 | render() { 20 | return ( 21 |
22 |

{this.props.message}

23 |
 {
 25 |             this.ref = ref;
 26 |           }}
 27 |           className="CodeMirror cm-s-jupyter CodeMirror-wrap"
 28 |         />
 29 |       
30 | ); 31 | } 32 | } 33 | 34 | /** 35 | * A widget for rendering jupyterlab_table files 36 | */ 37 | export class DocWidget extends Widget { 38 | constructor(context) { 39 | super(); 40 | this._context = context; 41 | this._onPathChanged(); 42 | this.addClass(CLASS_NAME); 43 | context.ready.then(() => { 44 | this.update(); 45 | /* Re-render when the document content changes */ 46 | context.model.contentChanged.connect(this.update, this); 47 | /* Re-render when the document path changes */ 48 | context.fileChanged.connect(this.update, this); 49 | }); 50 | /* Update title when path changes */ 51 | context.pathChanged.connect(this._onPathChanged, this); 52 | /* Throttle re-renders until changes have stopped */ 53 | this._monitor = new ActivityMonitor({ 54 | signal: context.model.contentChanged, 55 | timeout: RENDER_TIMEOUT 56 | }); 57 | this._monitor.activityStopped.connect(this.update, this); 58 | } 59 | 60 | /** 61 | * The widget's context 62 | */ 63 | get context() { 64 | return this._context; 65 | } 66 | 67 | /** 68 | * Dispose of the resources used by the widget 69 | */ 70 | dispose() { 71 | if (!this.isDisposed) { 72 | this._context = null; 73 | this._monitor.dispose(); 74 | super.dispose(); 75 | } 76 | } 77 | 78 | /** 79 | * A message handler invoked on an `'after-attach'` message 80 | */ 81 | onAfterAttach(msg) { 82 | /* Render initial data */ 83 | this.update(); 84 | } 85 | 86 | /** 87 | * A message handler invoked on an `'before-detach'` message 88 | */ 89 | onBeforeDetach(msg) { 90 | /* Dispose of resources used by widget */ 91 | ReactDOM.unmountComponentAtNode(this.node); 92 | } 93 | 94 | /** 95 | * A message handler invoked on a `'resize'` message 96 | */ 97 | onResize(msg) { 98 | /* Re-render on resize */ 99 | this.update(); 100 | } 101 | 102 | /** 103 | * A message handler invoked on an `'update-request'` message 104 | */ 105 | onUpdateRequest(msg) { 106 | if (this.isAttached && this._context.isReady) this._render(); 107 | } 108 | 109 | _render() { 110 | const content = this._context.model.toString(); 111 | try { 112 | const { data, schema } = JSON.parse(content); 113 | const props = { 114 | data, 115 | schema, 116 | width: this.node.offsetWidth, 117 | height: this.node.offsetHeight, 118 | fontSize: 13 119 | }; 120 | ReactDOM.render(, this.node); 121 | } catch (error) { 122 | ReactDOM.render( 123 | , 124 | this.node 125 | ); 126 | } 127 | } 128 | 129 | _onPathChanged() { 130 | this.title.label = this._context.path.split('/').pop(); 131 | } 132 | } 133 | 134 | /** 135 | * A widget factory for DocWidget 136 | */ 137 | export class DocWidgetFactory extends ABCWidgetFactory { 138 | /** 139 | * Create a new widget instance 140 | */ 141 | createNewWidget(context) { 142 | return new DocWidget(context); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /nbextension/src/renderer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import ReactDOMServer from 'react-dom/server'; 4 | import { VirtualizedGrid, VirtualizedTable } from 'jupyterlab_table_react'; 5 | import './index.css'; 6 | 7 | const MIME_TYPE = 'application/vnd.dataresource+json'; 8 | const CLASS_NAME = 'output_JSONTable rendered_html'; 9 | const DEFAULT_WIDTH = 840; 10 | const DEFAULT_HEIGHT = 360; 11 | 12 | /** 13 | * Render data to the DOM node 14 | */ 15 | function render(props, node) { 16 | ReactDOM.render(, node); 17 | } 18 | 19 | /** 20 | * Handle when an output is cleared or removed 21 | */ 22 | function handleClearOutput(event, { cell: { output_area } }) { 23 | /* Get rendered DOM node */ 24 | const toinsert = output_area.element.find(`.${CLASS_NAME.split(' ')[0]}`); 25 | /* e.g. Dispose of resources used by renderer library */ 26 | if (toinsert[0]) ReactDOM.unmountComponentAtNode(toinsert[0]); 27 | } 28 | 29 | /** 30 | * Handle when a new output is added 31 | */ 32 | function handleAddOutput(event, { output, output_area }) { 33 | /* Get rendered DOM node */ 34 | const toinsert = output_area.element.find(`.${CLASS_NAME.split(' ')[0]}`); 35 | /** e.g. Inject a static image representation into the mime bundle for 36 | * endering on Github, etc. 37 | */ 38 | // if (toinsert[0]) { 39 | // renderLibrary.toPng(toinsert[0]).then(url => { 40 | // const data = url.split(',')[1]; 41 | // output_area.outputs 42 | // .filter(output => output.data[MIME_TYPE]) 43 | // .forEach(output => { 44 | // output.data['image/png'] = data; 45 | // }); 46 | // }); 47 | // } 48 | } 49 | 50 | /** 51 | * Register the mime type and append_mime function with the notebook's 52 | * output area 53 | */ 54 | export function register_renderer(notebook, events, OutputArea) { 55 | /* A function to render output of 'application/vnd.dataresource+json' mime type */ 56 | const append_mime = function(data, metadata, element) { 57 | /* Create a DOM node to render to */ 58 | const toinsert = this.create_output_subarea( 59 | metadata, 60 | CLASS_NAME, 61 | MIME_TYPE 62 | ); 63 | this.keyboard_manager.register_events(toinsert); 64 | const type = metadata[MIME_TYPE] && 65 | metadata[MIME_TYPE].format && 66 | metadata[MIME_TYPE].format === 'table' 67 | ? VirtualizedTable 68 | : VirtualizedGrid; 69 | const props = { 70 | ...data, 71 | metadata: metadata[MIME_TYPE], 72 | width: element.width(), 73 | height: DEFAULT_HEIGHT, 74 | fontSize: 14 75 | }; 76 | ReactDOM.render(React.createElement(type, props), toinsert[0]); 77 | element.append(toinsert); 78 | const output_area = this; 79 | this.element.on('changed', () => { 80 | if (output_area.outputs.length > 0) ReactDOM.unmountComponentAtNode(toinsert[0]); 81 | }); 82 | return toinsert; 83 | }; 84 | 85 | /* Handle when an output is cleared or removed */ 86 | events.on('clear_output.CodeCell', handleClearOutput); 87 | events.on('delete.Cell', handleClearOutput); 88 | 89 | /* Handle when a new output is added */ 90 | events.on('output_added.OutputArea', handleAddOutput); 91 | 92 | /** 93 | * Calculate the index of this renderer in `output_area.display_order` 94 | * e.g. Insert this renderer after any renderers with mime type that matches 95 | * "+json" 96 | */ 97 | // const mime_types = output_area.mime_types(); 98 | // const json_types = mime_types.filter(mimetype => mimetype.includes('+json')); 99 | // const index = mime_types.lastIndexOf(json_types.pop() + 1); 100 | 101 | /* ...or just insert it at the top */ 102 | const index = 0; 103 | 104 | /** 105 | * Register the mime type and append_mime function with output_area 106 | */ 107 | OutputArea.prototype.register_mime_type(MIME_TYPE, append_mime, { 108 | /* Is output safe? */ 109 | safe: true, 110 | /* Index of renderer in `output_area.display_order` */ 111 | index: index 112 | }); 113 | } 114 | 115 | /** 116 | * Re-render cells with output data of 'application/vnd.dataresource+json' mime type 117 | * on load notebook 118 | */ 119 | export function render_cells(notebook) { 120 | /* Get all cells in notebook */ 121 | notebook.get_cells().forEach(cell => { 122 | /* If a cell has output data of 'application/vnd.dataresource+json' mime type */ 123 | if ( 124 | cell.output_area && 125 | cell.output_area.outputs.find( 126 | output => output.data && output.data[MIME_TYPE] 127 | ) 128 | ) { 129 | /* Re-render the cell */ 130 | notebook.render_cell_output(cell); 131 | } 132 | }); 133 | } 134 | -------------------------------------------------------------------------------- /component/virtualized-grid.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import React from 'react'; 3 | import { MultiGrid, AutoSizer } from 'react-virtualized'; 4 | // hack: `stream.Transform` (stream-browserify) is undefined in `csv-parse` when 5 | // built with @jupyterlabextension-builder 6 | import infer from 'jsontableschema/lib/infer'; 7 | // import { infer } from 'jsontableschema'; 8 | import './index.css'; 9 | 10 | const ROW_HEIGHT = 36; 11 | const COLUMN_WIDTH = 72; 12 | const GRID_MAX_HEIGHT = ROW_HEIGHT * 10; 13 | // The width per text character for calculating widths for columns 14 | const COLUMN_CHARACTER_WIDTH = 14; 15 | // The number of sample rows that should be used to infer types for columns 16 | // and widths for columns 17 | const SAMPLE_SIZE = 10; 18 | 19 | type Props = { 20 | data: Array, 21 | schema: { fields: Array }, 22 | theme: string 23 | }; 24 | 25 | type State = { 26 | data: Array, 27 | schema: { fields: Array }, 28 | columnWidths: Array 29 | }; 30 | 31 | function getSampleRows(data: Array, sampleSize: number): Array { 32 | return Array.from({ length: sampleSize }, () => { 33 | const index = Math.floor(Math.random() * data.length); 34 | return data[index]; 35 | }); 36 | } 37 | 38 | function inferSchema(data: Array): { fields: Array } { 39 | const sampleRows = getSampleRows(data, SAMPLE_SIZE); 40 | const headers = Array.from( 41 | sampleRows.reduce( 42 | (result, row) => new Set([...result, ...Object.keys(row)]), 43 | new Set() 44 | ) 45 | ); 46 | const values = sampleRows.map(row => Object.values(row)); 47 | return infer(headers, values); 48 | } 49 | 50 | function getState(props: Props) { 51 | const data = props.data; 52 | const schema = props.schema || inferSchema(data); 53 | const columns = schema.fields.map(field => field.name); 54 | const headers = columns.reduce( 55 | (result, column) => ({ ...result, [column]: column }), 56 | {} 57 | ); 58 | const columnWidths = columns.map(column => { 59 | const sampleRows = getSampleRows(data, SAMPLE_SIZE); 60 | return [headers, ...sampleRows].reduce( 61 | (result, row) => 62 | `${row[column]}`.length > result ? `${row[column]}`.length : result, 63 | Math.ceil(COLUMN_WIDTH / getCharacterWidth(COLUMN_CHARACTER_WIDTH)) 64 | ); 65 | }); 66 | return { 67 | data: [headers, ...data], 68 | schema, 69 | columnWidths 70 | }; 71 | } 72 | 73 | function getCharacterWidth(fontSize) { 74 | return fontSize * 0.86; 75 | } 76 | 77 | export default class VirtualizedGrid extends React.Component { 78 | props: Props; 79 | state: State = { 80 | data: [], 81 | schema: { fields: [] }, 82 | columnWidths: [] 83 | }; 84 | 85 | componentWillMount() { 86 | const state = getState(this.props); 87 | this.setState(state); 88 | } 89 | 90 | componentWillReceiveProps(nextProps: Props) { 91 | const state = getState(nextProps); 92 | this.setState(state); 93 | } 94 | 95 | cellRenderer = ( 96 | { 97 | columnIndex, 98 | key, 99 | parent, 100 | rowIndex, 101 | style 102 | }: { 103 | columnIndex: number, 104 | key: string, 105 | parent: mixed, 106 | rowIndex: number, 107 | style: Object 108 | } 109 | ) => { 110 | const { name: column, type } = this.state.schema.fields[columnIndex]; 111 | const value = this.state.data[rowIndex][column]; 112 | return ( 113 |
118 | {value} 119 |
120 | ); 121 | }; 122 | 123 | render() { 124 | const rowCount = this.state.data.length; 125 | const height = rowCount * ROW_HEIGHT; 126 | return ( 127 | 128 | {({ width }) => ( 129 | 133 | this.state.columnWidths[index] * 134 | getCharacterWidth( 135 | this.props.fontSize || COLUMN_CHARACTER_WIDTH 136 | ) || COLUMN_WIDTH} 137 | fixedColumnCount={1} 138 | fixedRowCount={1} 139 | height={this.props.height > height ? height : this.props.height} 140 | overscanColumnCount={15} 141 | overscanRowCount={150} 142 | rowCount={rowCount} 143 | rowHeight={ROW_HEIGHT} 144 | width={this.props.width || width} 145 | /> 146 | )} 147 | 148 | ); 149 | } 150 | } 151 | 152 | const styles = { 153 | cell: ({ columnIndex, rowIndex, style, type }) => ({ 154 | ...style, 155 | boxSizing: 'border-box', 156 | padding: '0.5em 1em', 157 | border: '1px solid #ddd', 158 | overflow: 'hidden', 159 | whiteSpace: 'nowrap', 160 | textOverflow: 'ellipsis', 161 | // Remove top border for all cells except first row 162 | ...(rowIndex !== 0 ? { borderTop: 'none' } : {}), 163 | // Remove left border for all cells except first column 164 | ...(columnIndex !== 0 ? { borderLeft: 'none' } : {}), 165 | // Highlight even rows 166 | ...(rowIndex % 2 === 0 && !(rowIndex === 0 || columnIndex === 0) 167 | ? { background: 'rgba(0, 0, 0, 0.03)' } 168 | : {}), 169 | // Bold the headers 170 | ...(rowIndex === 0 || columnIndex === 0 171 | ? { 172 | background: 'rgba(0, 0, 0, 0.06)', 173 | fontWeight: 'bold' 174 | } 175 | : {}), 176 | // Right-align numbers 177 | ...(!(rowIndex === 0 || columnIndex === 0) && 178 | (type === 'number' || type === 'integer') 179 | ? { textAlign: 'right' } 180 | : { textAlign: 'left' }) 181 | }) 182 | }; 183 | -------------------------------------------------------------------------------- /component/virtualized-table.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import React from 'react'; 3 | import { Table, Column, SortDirection, AutoSizer } from 'react-virtualized'; 4 | // hack: `stream.Transform` (stream-browserify) is undefined in `csv-parse` when 5 | // built with @jupyterlabextension-builder 6 | import infer from 'jsontableschema/lib/infer'; 7 | // import { infer } from 'jsontableschema'; 8 | import "./index.css"; 9 | 10 | const ROW_HEIGHT = 36; 11 | const TABLE_MAX_HEIGHT = ROW_HEIGHT * 10; 12 | 13 | type Props = { 14 | data: Array, 15 | schema: { fields: Array }, 16 | theme: string 17 | }; 18 | 19 | type State = { 20 | data: Array, 21 | schema: { fields: Array }, 22 | sortBy: string, 23 | sortDirection: string 24 | }; 25 | 26 | function getSampleRows(data: Array, sampleSize: number): Array { 27 | return Array.from({ length: sampleSize }, () => { 28 | const index = Math.floor(Math.random() * data.length); 29 | return data[index]; 30 | }); 31 | } 32 | 33 | function inferSchema(data: Array): { fields: Array } { 34 | // Take a sampling of rows from data 35 | const range = Array.from({ length: 10 }, () => 36 | Math.floor(Math.random() * data.length)); 37 | // Separate headers and values 38 | const headers = Array.from( 39 | range.reduce( 40 | (result, row) => new Set([...result, ...Object.keys(data[row])]), 41 | new Set() 42 | ) 43 | ); 44 | const values = range.map(row => Object.values(data[row])); 45 | // Infer column types and return schema for data 46 | return infer(headers, values); 47 | } 48 | 49 | function getState(props: Props) { 50 | const data = props.data; 51 | const schema = props.schema || inferSchema(data); 52 | return { 53 | data, 54 | schema 55 | }; 56 | } 57 | 58 | export default class VirtualizedTable extends React.Component { 59 | props: Props; 60 | state: State = { 61 | data: [], 62 | schema: { fields: [] }, 63 | sortBy: '', 64 | sortDirection: SortDirection.ASC 65 | }; 66 | 67 | componentWillMount() { 68 | const state = getState(this.props); 69 | this.setState(state); 70 | } 71 | 72 | componentWillReceiveProps(nextProps: Props) { 73 | const state = getState(nextProps); 74 | this.setState(state); 75 | } 76 | 77 | render() { 78 | const rowCount = this.state.data.length + 1; 79 | const height = rowCount * ROW_HEIGHT; 80 | return ( 81 | 82 | {({ width }) => ( 83 | this.ref = ref} 85 | className="table" 86 | // disableHeader={disableHeader} 87 | // headerClassName="th" 88 | headerHeight={ROW_HEIGHT} 89 | headerStyle={styles.header} 90 | height={ 91 | this.props.height || height < TABLE_MAX_HEIGHT 92 | ? height 93 | : TABLE_MAX_HEIGHT 94 | } 95 | // noRowsRenderer={this._noRowsRenderer} 96 | // overscanRowCount={overscanRowCount} 97 | rowClassName={({ index }) => index === -1 ? 'th' : 'tr'} 98 | rowHeight={ROW_HEIGHT} 99 | rowGetter={({ index }) => this.state.data[index]} 100 | rowCount={rowCount} 101 | rowStyle={styles.row} 102 | // scrollToIndex={scrollToIndex} 103 | sort={this.sort} 104 | sortBy={this.state.sortBy} 105 | sortDirection={this.state.sortDirection} 106 | style={styles.table} 107 | width={this.props.width || width} 108 | > 109 | {this.state.schema.fields.map((field, fieldIndex) => ( 110 | ( 112 | // 113 | // )} 114 | // // cellDataGetter={({ columnData, dataKey, rowData }) => 115 | // rowData 116 | // } 117 | // disableSort={!this._isSortEnabled()} 118 | dataKey={field.name} 119 | key={fieldIndex} 120 | flexGrow={1} 121 | flexShrink={1} 122 | label={`${field.name}`} 123 | style={styles.column({ type: field.type, index: fieldIndex })} 124 | width={150} 125 | /> 126 | ))} 127 |
128 | )} 129 |
130 | ); 131 | } 132 | 133 | sort = ({ sortBy, sortDirection }) => { 134 | if (this.state.sortDirection === SortDirection.DESC) { 135 | this.setState({ 136 | data: this.props.data, 137 | sortBy: null, 138 | sortDirection: null 139 | }); 140 | } else { 141 | const { type } = this.state.schema.fields.find( 142 | field => field.name === sortBy 143 | ); 144 | const data = [...this.props.data].sort((a, b) => { 145 | if (type === 'date' || type === 'time' || type === 'datetime') { 146 | return sortDirection === SortDirection.ASC 147 | ? new Date(a[sortBy]) - new Date(b[sortBy]) 148 | : new Date(b[sortBy]) - new Date(a[sortBy]); 149 | } 150 | if (type === 'number' || type === 'integer') { 151 | return sortDirection === SortDirection.ASC 152 | ? parseInt(a[sortBy]) - parseInt(b[sortBy]) 153 | : parseInt(b[sortBy]) - parseInt(a[sortBy]); 154 | } 155 | if (type === 'string') { 156 | return sortDirection === SortDirection.ASC 157 | ? a[sortBy].toLowerCase() > b[sortBy].toLowerCase() ? 1 : -1 158 | : b[sortBy].toLowerCase() > a[sortBy].toLowerCase() ? 1 : -1; 159 | } 160 | }); 161 | this.setState({ data, sortBy, sortDirection }); 162 | } 163 | }; 164 | } 165 | 166 | const styles = { 167 | table: { 168 | boxSizing: 'border-box' 169 | }, 170 | header: { 171 | fontWeight: 'bold', 172 | textAlign: 'right', 173 | overflow: 'hidden', 174 | whiteSpace: 'nowrap', 175 | textOverflow: 'ellipsis', 176 | padding: '0.5em 1em', 177 | border: '1px solid #ddd' 178 | }, 179 | row: ({ index }) => ({ 180 | display: 'flex', 181 | flexDirection: 'row', 182 | alignItems: 'center', 183 | justifyContent: 'flex-start', 184 | background: index % 2 === 0 || index === -1 185 | ? 'transparent' 186 | : 'rgba(0, 0, 0, 0.03)' 187 | }), 188 | column: ({ type, index }) => ({ 189 | textAlign: type === 'number' || type === 'integer' ? 'right' : 'left', 190 | overflow: 'hidden', 191 | whiteSpace: 'nowrap', 192 | textOverflow: 'ellipsis', 193 | padding: '0.5em 1em', 194 | borderRight: '1px solid #ddd', 195 | borderBottom: '1px solid #ddd', 196 | borderLeft: index === 0 ? '1px solid #ddd' : 'none' 197 | }) 198 | }; 199 | -------------------------------------------------------------------------------- /jupyterlab_table/__init__.py: -------------------------------------------------------------------------------- 1 | from IPython.display import display, DisplayObject 2 | import json 3 | import pandas as pd 4 | 5 | 6 | # Running `npm run build` will create static resources in the static 7 | # directory of this Python package (and create that directory if necessary). 8 | 9 | def _jupyter_labextension_paths(): 10 | return [{ 11 | 'name': 'jupyterlab_table', 12 | 'src': 'static', 13 | }] 14 | 15 | def _jupyter_nbextension_paths(): 16 | return [{ 17 | 'section': 'notebook', 18 | 'src': 'static', 19 | 'dest': 'jupyterlab_table', 20 | 'require': 'jupyterlab_table/extension' 21 | }] 22 | 23 | def prepare_data(data=None, schema=None): 24 | """Prepare JSONTable data from Pandas DataFrame.""" 25 | 26 | if isinstance(data, pd.DataFrame): 27 | data = data.to_json(orient='table') 28 | return json.loads(data) 29 | return { 30 | 'data': data, 31 | 'schema': schema 32 | } 33 | 34 | # A display class that can be used within a notebook. E.g.: 35 | # from jupyterlab_table import JSONTable 36 | # JSONTable(data, schema) 37 | 38 | class JSONTable(DisplayObject): 39 | """A display class for displaying JSONTable visualizations in the Jupyter Notebook and IPython kernel. 40 | 41 | JSONTable expects a JSON-able list, not serialized JSON strings. 42 | 43 | Scalar types (None, number, string) are not allowed, only dict containers. 44 | """ 45 | # wrap data in a property, which warns about passing already-serialized JSON 46 | _data = None 47 | _schema = None 48 | def __init__(self, data=None, schema=None, url=None, filename=None, metadata=None): 49 | """Create a JSON Table display object given raw data. 50 | 51 | Parameters 52 | ---------- 53 | data : list 54 | Not an already-serialized JSON string. 55 | Scalar types (None, number, string) are not allowed, only list containers. 56 | schema : dict 57 | JSON Table Schema. See http://frictionlessdata.io/guides/json-table-schema/. 58 | url : unicode 59 | A URL to download the data from. 60 | filename : unicode 61 | Path to a local file to load the data from. 62 | metadata: dict 63 | Specify extra metadata to attach to the json display object. 64 | """ 65 | self.schema = schema 66 | self.metadata = metadata 67 | super(JSONTable, self).__init__(data=data, url=url, filename=filename) 68 | 69 | def _check_data(self): 70 | if self.data is not None and not isinstance(self.data, (list, pd.DataFrame)): 71 | raise TypeError("%s expects a JSONable list or pandas DataFrame, not %r" % (self.__class__.__name__, self.data)) 72 | if self.schema is not None and not isinstance(self.schema, dict): 73 | raise TypeError("%s expects a JSONable dict, not %r" % (self.__class__.__name__, self.schema)) 74 | 75 | @property 76 | def data(self): 77 | return self._data 78 | 79 | @property 80 | def schema(self): 81 | return self._schema 82 | 83 | @data.setter 84 | def data(self, data): 85 | if isinstance(data, str): 86 | # warnings.warn("JSONTable expects JSON-able dict or list, not JSON strings") 87 | data = json.loads(data) 88 | self._data = data 89 | 90 | @schema.setter 91 | def schema(self, schema): 92 | if isinstance(schema, str): 93 | # warnings.warn("JSONTable expects a JSON-able list, not JSON strings") 94 | schema = json.loads(schema) 95 | self._schema = schema 96 | 97 | def _ipython_display_(self): 98 | bundle = { 99 | 'application/vnd.dataresource+json': prepare_data(self.data, self.schema), 100 | 'text/plain': '' 101 | } 102 | metadata = { 103 | 'application/vnd.dataresource+json': self.metadata 104 | } 105 | display(bundle, metadata=metadata, raw=True) 106 | 107 | class Grid(JSONTable): 108 | def __init__(self, data=None, schema=None, url=None, filename=None, metadata=None): 109 | """Create a JSON Table display object given raw data. 110 | 111 | Parameters 112 | ---------- 113 | data : list 114 | Not an already-serialized JSON string. 115 | Scalar types (None, number, string) are not allowed, only list containers. 116 | schema : dict 117 | JSON Table Schema. See http://frictionlessdata.io/guides/json-table-schema/. 118 | url : unicode 119 | A URL to download the data from. 120 | filename : unicode 121 | Path to a local file to load the data from. 122 | metadata: dict 123 | Specify extra metadata to attach to the json display object. 124 | """ 125 | self.schema = schema 126 | self.metadata = metadata 127 | super(JSONTable, self).__init__(data=data, url=url, filename=filename) 128 | 129 | def _ipython_display_(self): 130 | metadata = {'format': 'grid'} 131 | if self.metadata: 132 | metadata.update(self.metadata) 133 | bundle = { 134 | 'application/vnd.dataresource+json': prepare_data(self.data, self.schema), 135 | 'text/plain': '' 136 | } 137 | metadata = { 138 | 'application/vnd.dataresource+json': metadata 139 | } 140 | display(bundle, metadata=metadata, raw=True) 141 | 142 | class Table(JSONTable): 143 | def __init__(self, data=None, schema=None, url=None, filename=None, metadata=None): 144 | """Create a JSON Table display object given raw data. 145 | 146 | Parameters 147 | ---------- 148 | data : list 149 | Not an already-serialized JSON string. 150 | Scalar types (None, number, string) are not allowed, only list containers. 151 | schema : dict 152 | JSON Table Schema. See http://frictionlessdata.io/guides/json-table-schema/. 153 | url : unicode 154 | A URL to download the data from. 155 | filename : unicode 156 | Path to a local file to load the data from. 157 | metadata: dict 158 | Specify extra metadata to attach to the json display object. 159 | """ 160 | self.schema = schema 161 | self.metadata = metadata 162 | super(JSONTable, self).__init__(data=data, url=url, filename=filename) 163 | 164 | def _ipython_display_(self): 165 | metadata = {'format': 'table'} 166 | if self.metadata: 167 | metadata.update(self.metadata) 168 | bundle = { 169 | 'application/vnd.dataresource+json': prepare_data(self.data, self.schema), 170 | 'text/plain': '' 171 | } 172 | metadata = { 173 | 'application/vnd.dataresource+json': metadata 174 | } 175 | display(bundle, metadata=metadata, raw=True) 176 | -------------------------------------------------------------------------------- /setupbase.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | # Copyright (c) Jupyter Development Team. 5 | # Distributed under the terms of the Modified BSD License. 6 | 7 | import os 8 | from os.path import join as pjoin 9 | import functools 10 | import pipes 11 | 12 | from setuptools import Command 13 | from setuptools.command.build_py import build_py 14 | from setuptools.command.sdist import sdist 15 | from setuptools.command.develop import develop 16 | from setuptools.command.bdist_egg import bdist_egg 17 | from distutils import log 18 | from subprocess import check_call 19 | import sys 20 | 21 | try: 22 | from wheel.bdist_wheel import bdist_wheel 23 | except ImportError: 24 | bdist_wheel = None 25 | 26 | if sys.platform == 'win32': 27 | from subprocess import list2cmdline 28 | else: 29 | def list2cmdline(cmd_list): 30 | return ' '.join(map(pipes.quote, cmd_list)) 31 | 32 | 33 | __version__ = '0.1.0' 34 | 35 | # --------------------------------------------------------------------------- 36 | # Top Level Variables 37 | # --------------------------------------------------------------------------- 38 | 39 | 40 | here = os.path.abspath(os.path.dirname(sys.argv[0])) 41 | is_repo = os.path.exists(pjoin(here, '.git')) 42 | node_modules = pjoin(here, 'node_modules') 43 | npm_path = ':'.join([ 44 | pjoin(here, 'node_modules', '.bin'), 45 | os.environ.get('PATH', os.defpath), 46 | ]) 47 | 48 | if "--skip-npm" in sys.argv: 49 | print("Skipping npm install as requested.") 50 | skip_npm = True 51 | sys.argv.remove("--skip-npm") 52 | else: 53 | skip_npm = False 54 | 55 | # --------------------------------------------------------------------------- 56 | # Public Functions 57 | # --------------------------------------------------------------------------- 58 | 59 | 60 | def get_data_files(top): 61 | """Get data files""" 62 | 63 | data_files = [] 64 | ntrim = len(here + os.path.sep) 65 | 66 | for (d, dirs, filenames) in os.walk(top): 67 | data_files.append(( 68 | d[ntrim:], 69 | [pjoin(d, f) for f in filenames] 70 | )) 71 | return data_files 72 | 73 | 74 | def find_packages(top): 75 | """ 76 | Find all of the packages. 77 | """ 78 | packages = [] 79 | for d, _, _ in os.walk(top): 80 | if os.path.exists(pjoin(d, '__init__.py')): 81 | packages.append(d.replace(os.path.sep, '.')) 82 | 83 | 84 | def create_cmdclass(wrappers=None, data_dirs=None): 85 | """Create a command class with the given optional wrappers. 86 | Parameters 87 | ---------- 88 | wrappers: list(str), optional 89 | The cmdclass names to run before running other commands 90 | data_dirs: list(str), optional. 91 | The directories containing static data. 92 | """ 93 | egg = bdist_egg if 'bdist_egg' in sys.argv else bdist_egg_disabled 94 | wrappers = wrappers or [] 95 | data_dirs = data_dirs or [] 96 | wrapper = functools.partial(wrap_command, wrappers, data_dirs) 97 | cmdclass = dict( 98 | build_py=wrapper(build_py, strict=is_repo), 99 | sdist=wrapper(sdist, strict=True), 100 | bdist_egg=egg, 101 | develop=wrapper(develop, strict=True) 102 | ) 103 | if bdist_wheel: 104 | cmdclass['bdist_wheel'] = wrapper(bdist_wheel, strict=True) 105 | return cmdclass 106 | 107 | 108 | def run(cmd, *args, **kwargs): 109 | """Echo a command before running it. Defaults to repo as cwd""" 110 | log.info('> ' + list2cmdline(cmd)) 111 | kwargs.setdefault('cwd', here) 112 | kwargs.setdefault('shell', sys.platform == 'win32') 113 | if not isinstance(cmd, list): 114 | cmd = cmd.split() 115 | return check_call(cmd, *args, **kwargs) 116 | 117 | 118 | def is_stale(target, source): 119 | """Test whether the target file/directory is stale based on the source 120 | file/directory. 121 | """ 122 | if not os.path.exists(target): 123 | return True 124 | return mtime(target) < mtime(source) 125 | 126 | 127 | class BaseCommand(Command): 128 | """Empty command because Command needs subclasses to override too much""" 129 | user_options = [] 130 | 131 | def initialize_options(self): 132 | pass 133 | 134 | def finalize_options(self): 135 | pass 136 | 137 | def get_inputs(self): 138 | return [] 139 | 140 | def get_outputs(self): 141 | return [] 142 | 143 | 144 | def combine_commands(*commands): 145 | """Return a Command that combines several commands.""" 146 | 147 | class CombinedCommand(Command): 148 | 149 | def initialize_options(self): 150 | self.commands = [] 151 | for C in commands: 152 | self.commands.append(C(self.distribution)) 153 | for c in self.commands: 154 | c.initialize_options() 155 | 156 | def finalize_options(self): 157 | for c in self.commands: 158 | c.finalize_options() 159 | 160 | def run(self): 161 | for c in self.commands: 162 | c.run() 163 | return CombinedCommand 164 | 165 | 166 | def mtime(path): 167 | """shorthand for mtime""" 168 | return os.stat(path).st_mtime 169 | 170 | 171 | def install_npm(path=None, build_dir=None, source_dir=None, build_cmd='build'): 172 | """Return a Command for managing an npm installation. 173 | Parameters 174 | ---------- 175 | path: str, optional 176 | The base path of the node package. Defaults to the repo root. 177 | build_dir: str, optional 178 | The target build directory. If this and source_dir are given, 179 | the JavaScript will only be build if necessary. 180 | source_dir: str, optional 181 | The source code directory. 182 | build_cmd: str, optional 183 | The npm command to build assets to the build_dir. 184 | """ 185 | 186 | class NPM(BaseCommand): 187 | description = 'install package.json dependencies using npm' 188 | 189 | def run(self): 190 | if skip_npm: 191 | log.info('Skipping npm-installation') 192 | return 193 | node_package = path or here 194 | node_modules = pjoin(node_package, 'node_modules') 195 | 196 | if not which("npm"): 197 | log.error("`npm` unavailable. If you're running this command " 198 | "using sudo, make sure `npm` is availble to sudo") 199 | return 200 | if is_stale(node_modules, pjoin(node_package, 'package.json')): 201 | log.info('Installing build dependencies with npm. This may ' 202 | 'take a while...') 203 | run(['npm', 'install'], cwd=node_package) 204 | if build_dir and source_dir: 205 | should_build = is_stale(build_dir, source_dir) 206 | else: 207 | should_build = True 208 | if should_build: 209 | run(['npm', 'run', build_cmd], cwd=node_package) 210 | 211 | return NPM 212 | 213 | 214 | # `shutils.which` function copied verbatim from the Python-3.3 source. 215 | def which(cmd, mode=os.F_OK | os.X_OK, path=None): 216 | """Given a command, mode, and a PATH string, return the path which 217 | conforms to the given mode on the PATH, or None if there is no such 218 | file. 219 | `mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result 220 | of os.environ.get("PATH"), or can be overridden with a custom search 221 | path. 222 | """ 223 | 224 | # Check that a given file can be accessed with the correct mode. 225 | # Additionally check that `file` is not a directory, as on Windows 226 | # directories pass the os.access check. 227 | def _access_check(fn, mode): 228 | return (os.path.exists(fn) and os.access(fn, mode) and 229 | not os.path.isdir(fn)) 230 | 231 | # Short circuit. If we're given a full path which matches the mode 232 | # and it exists, we're done here. 233 | if _access_check(cmd, mode): 234 | return cmd 235 | 236 | path = (path or os.environ.get("PATH", os.defpath)).split(os.pathsep) 237 | 238 | if sys.platform == "win32": 239 | # The current directory takes precedence on Windows. 240 | if os.curdir not in path: 241 | path.insert(0, os.curdir) 242 | 243 | # PATHEXT is necessary to check on Windows. 244 | pathext = os.environ.get("PATHEXT", "").split(os.pathsep) 245 | # See if the given file matches any of the expected path extensions. 246 | # This will allow us to short circuit when given "python.exe". 247 | matches = [cmd for ext in pathext if cmd.lower().endswith(ext.lower())] 248 | # If it does match, only test that one, otherwise we have to try 249 | # others. 250 | files = [cmd] if matches else [cmd + ext.lower() for ext in pathext] 251 | else: 252 | # On other platforms you don't have things like PATHEXT to tell you 253 | # what file suffixes are executable, so just pass on cmd as-is. 254 | files = [cmd] 255 | 256 | seen = set() 257 | for dir in path: 258 | dir = os.path.normcase(dir) 259 | if dir not in seen: 260 | seen.add(dir) 261 | for thefile in files: 262 | name = os.path.join(dir, thefile) 263 | if _access_check(name, mode): 264 | return name 265 | return None 266 | 267 | 268 | # --------------------------------------------------------------------------- 269 | # Private Functions 270 | # --------------------------------------------------------------------------- 271 | 272 | 273 | def wrap_command(cmds, data_dirs, cls, strict=True): 274 | """Wrap a setup command 275 | Parameters 276 | ---------- 277 | cmds: list(str) 278 | The names of the other commands to run prior to the command. 279 | strict: boolean, optional 280 | Wether to raise errors when a pre-command fails. 281 | """ 282 | class WrappedCommand(cls): 283 | 284 | def run(self): 285 | if not getattr(self, 'uninstall', None): 286 | try: 287 | [self.run_command(cmd) for cmd in cmds] 288 | except Exception: 289 | if strict: 290 | raise 291 | else: 292 | pass 293 | 294 | result = cls.run(self) 295 | data_files = [] 296 | for dname in data_dirs: 297 | data_files.extend(get_data_files(dname)) 298 | # update data-files in case this created new files 299 | self.distribution.data_files = data_files 300 | return result 301 | return WrappedCommand 302 | 303 | 304 | class bdist_egg_disabled(bdist_egg): 305 | """Disabled version of bdist_egg 306 | Prevents setup.py install performing setuptools' default easy_install, 307 | which it should never ever do. 308 | """ 309 | 310 | def run(self): 311 | sys.exit("Aborting implicit building of eggs. Use `pip install .` " + 312 | " to install from source.") 313 | --------------------------------------------------------------------------------