Merge branch 'jupyter/master' into toggle-line-numbers

pull/1676/head
Grant Nestor 10 years ago
commit c2773bd72c

@ -0,0 +1,3 @@
{
"presets": ["es2015"],
}

@ -1,4 +0,0 @@
.git
.git*
.mailmap
.travis.yml

@ -0,0 +1,5 @@
*.min.js
*components*
*node_modules*
*built*
*build*

@ -0,0 +1,13 @@
{
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
},
"rules": {
"semi": 1,
"no-cond-assign": 2,
"no-debugger": 2,
"comma-dangle": 1,
"no-unreachable" : 2
}
}

@ -26,7 +26,8 @@ before_install:
- nvm install 5.6
- nvm use 5.6
- npm upgrade -g npm
- 'if [[ $GROUP == js* ]]; then npm install -g casperjs; fi'
- npm install
- 'if [[ $GROUP == js* ]]; then npm install -g casperjs@1.1.0-beta5; fi'
- git clone --quiet --depth 1 https://github.com/minrk/travis-wheels travis-wheels
install:
@ -34,6 +35,7 @@ install:
script:
- 'if [[ $GROUP == js* ]]; then travis_retry python -m notebook.jstest ${GROUP:3}; fi'
- 'if [[ $GROUP == "js/notebook" ]]; then npm run lint; fi'
- 'if [[ $GROUP == python ]]; then nosetests -v --with-coverage --cover-package=notebook notebook; fi'
matrix:

@ -14,17 +14,12 @@ General Guidelines
For general documentation about contributing to Jupyter projects, see the
`Project Jupyter Contributor Documentation`__.
__ http://jupyter.readthedocs.org/#contributor-documentation
__ http://jupyter.readthedocs.io/en/latest/contributor/content-contributor.html
Setting Up a Development Environment
------------------------------------
For general installation instructions have a look at the
`Project Jupyter Installation Guide`__.
__ https://jupyter.readthedocs.org/en/latest/install.html
Installing Node.js and npm
^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -64,6 +59,11 @@ If you want the development environment to be available for all users of your
system (assuming you have the necessary rights) or if you are installing in a
virtual environment, just drop the ``--user`` option.
Once you have done this, you can launch the master branch of Jupyter notebook
from any directory in your system with::
jupyter notebook
Rebuilding JavaScript and CSS
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -141,11 +141,11 @@ or to run just ``notebook/tests/notebook/deletecell.js``::
Building the Documentation
--------------------------
To build the documentation you'll need `Sphinx <http://www.sphinx-doc.org/>`_
To build the documentation you'll need `Sphinx <http://www.sphinx-doc.org/>`_, `pandoc <http://pandoc.org/>`_
and a few other packages.
To install (and activate) a `conda environment`_ named ``notebook_docs``
containing all the necessary packages, use::
containing all the necessary packages (except pandoc), use::
conda env create -f docs/environment.yml
source activate notebook_docs # Linux and OS X
@ -175,4 +175,4 @@ Windows users can find ``make.bat`` in the ``docs`` folder.
You should also have a look at the `Project Jupyter Documentation Guide`__.
__ https://jupyter.readthedocs.org/en/latest/contrib_guide_docs.html
__ https://jupyter.readthedocs.io/en/latest/contrib_docs/index.html

@ -1,112 +0,0 @@
# Installs Jupyter Notebook and IPython kernel from the current branch
# Another Docker container should inherit with `FROM jupyter/notebook`
# to run actual services.
#
# For opinionated stacks of ready-to-run Jupyter applications in Docker,
# check out docker-stacks <https://github.com/jupyter/docker-stacks>
FROM jupyter/ubuntu_14_04_locale_fix
MAINTAINER Project Jupyter <jupyter@googlegroups.com>
# Not essential, but wise to set the lang
# Note: Users with other languages should set this in their derivative image
ENV LANGUAGE en_US.UTF-8
ENV LANG en_US.UTF-8
ENV LC_ALL en_US.UTF-8
ENV PYTHONIOENCODING UTF-8
# Remove preinstalled copy of python that blocks our ability to install development python.
RUN DEBIAN_FRONTEND=noninteractive apt-get remove -yq \
python3-minimal \
python3.4 \
python3.4-minimal \
libpython3-stdlib \
libpython3.4-stdlib \
libpython3.4-minimal
# Python binary and source dependencies
RUN apt-get update -qq && \
DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
build-essential \
ca-certificates \
curl \
git \
language-pack-en \
libcurl4-openssl-dev \
libffi-dev \
libsqlite3-dev \
libzmq3-dev \
pandoc \
python \
python3 \
python-dev \
python3-dev \
sqlite3 \
texlive-fonts-recommended \
texlive-latex-base \
texlive-latex-extra \
zlib1g-dev && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Install Tini
RUN curl -L https://github.com/krallin/tini/releases/download/v0.6.0/tini > tini && \
echo "d5ed732199c36a1189320e6c4859f0169e950692f451c03e7854243b95f4234b *tini" | sha256sum -c - && \
mv tini /usr/local/bin/tini && \
chmod +x /usr/local/bin/tini
# Install the recent pip release
RUN curl -O https://bootstrap.pypa.io/get-pip.py && \
python2 get-pip.py && \
python3 get-pip.py && \
rm get-pip.py && \
pip2 --no-cache-dir install requests[security] && \
pip3 --no-cache-dir install requests[security] && \
rm -rf /root/.cache
# Install some dependencies.
RUN pip2 --no-cache-dir install ipykernel && \
pip3 --no-cache-dir install ipykernel && \
\
python2 -m ipykernel.kernelspec && \
python3 -m ipykernel.kernelspec && \
rm -rf /root/.cache
# Move notebook contents into place.
ADD . /usr/src/jupyter-notebook
# Install dependencies and run tests.
RUN BUILD_DEPS="nodejs-legacy npm" && \
apt-get update -qq && \
DEBIAN_FRONTEND=noninteractive apt-get install -yq $BUILD_DEPS && \
\
pip3 install --no-cache-dir /usr/src/jupyter-notebook && \
pip2 install --no-cache-dir widgetsnbextension && \
pip3 install --no-cache-dir widgetsnbextension && \
\
npm cache clean && \
apt-get clean && \
rm -rf /root/.npm && \
rm -rf /root/.cache && \
rm -rf /root/.config && \
rm -rf /root/.local && \
rm -rf /root/tmp && \
rm -rf /var/lib/apt/lists/* && \
apt-get purge -y --auto-remove \
-o APT::AutoRemove::RecommendsImportant=false -o APT::AutoRemove::SuggestsImportant=false $BUILD_DEPS
# Run tests.
RUN pip3 install --no-cache-dir notebook[test] && nosetests -v notebook
# Add a notebook profile.
RUN mkdir -p -m 700 /root/.jupyter/ && \
echo "c.NotebookApp.ip = '*'" >> /root/.jupyter/jupyter_notebook_config.py
VOLUME /notebooks
WORKDIR /notebooks
EXPOSE 8888
ENTRYPOINT ["tini", "--"]
CMD ["jupyter", "notebook"]

@ -41,34 +41,6 @@ Launch with:
$ jupyter notebook
### Running in a Docker container
If you are using **Linux** and have a
[Docker daemon running](https://docs.docker.com/installation/),
e.g. reachable on `localhost`, start a container with:
$ docker run --rm -it -p 8888:8888 -v "$(pwd):/notebooks" jupyter/notebook
In your browser, open the URL `http://localhost:8888/`.
All notebooks from your session will be saved in the current directory.
On other platforms, such as **Windows and OS X**, that use
[`docker-machine`](https://docs.docker.com/machine/install-machine/) with `docker`, a container can be started using
`docker-machine`. In the browser, open the URL `http://ip:8888/` where `ip` is
the IP address returned from the command [`docker-machine ip <MACHINE>`](https://docs.docker.com/machine/reference/ip/):
$ docker-machine ip <MACHINE>
For example,
$ docker-machine ip myjupytermachine
192.168.99.104
In browser, open `http://192.168.99.104:8888`.
NOTE: With the deprecated `boot2docker`, use the command `boot2docker ip` to
determine the URL.
## Development Installation
See [`CONTRIBUTING.rst`](CONTRIBUTING.rst) for how to set up a local development installation.

@ -5,7 +5,7 @@
"backbone": "components/backbone#~1.2",
"bootstrap": "components/bootstrap#~3.3",
"bootstrap-tour": "0.9.0",
"codemirror": "~5.8",
"codemirror": "~5.14",
"es6-promise": "~1.0",
"font-awesome": "components/font-awesome#~4.2.0",
"google-caja": "5669",
@ -15,7 +15,6 @@
"MathJax": "components/MathJax#~2.6",
"moment": "~2.8.4",
"requirejs": "~2.1",
"term.js": "chjj/term.js#~0.0.7",
"text-encoding": "~0.1",
"underscore": "components/underscore#~1.5",
"jquery-typeahead": "~2.0.0"

@ -1,11 +0,0 @@
machine:
services:
- docker
dependencies:
override:
- docker info
test:
override:
- docker build -t jupyter/notebook .

@ -0,0 +1,9 @@
coverage:
status:
project:
default:
target: auto
threshold: 10
patch:
default:
target: 0%

@ -6,6 +6,74 @@ Jupyter notebook changelog
A summary of changes in the Jupyter notebook.
For more detailed information, see `GitHub <https://github.com/jupyter/notebook>`__.
.. tip::
Use ``pip install notebook --upgrade`` or ``conda upgrade notebook`` to
upgrade to the latest release.
.. _release-4.2.2:
4.2.2
-----
4.2.2 is a small bugfix release on 4.2, with an important security fix.
All users are strongly encouraged to upgrade to 4.2.2.
Highlights:
- **Security fix**: CVE-2016-6524, where untrusted latex output
could be added to the page in a way that could execute javascript.
- Fix missing POST in OPTIONS responses.
- Fix for downloading non-ascii filenames.
- Avoid clobbering ssl_options, so that users can specify more detailed SSL configuration.
- Fix inverted load order in nbconfig, so user config has highest priority.
- Improved error messages here and there.
.. seealso::
4.2.2 `on GitHub <https://github.com/jupyter/notebook/milestones/4.2.2>`__.
.. _release-4.2.1:
4.2.1
-----
4.2.1 is a small bugfix release on 4.2. Highlights:
- Compatibility fixes for some versions of ipywidgets
- Fix for ignored CSS on Windows
- Fix specifying destination when installing nbextensions
.. seealso::
4.2.1 `on GitHub <https://github.com/jupyter/notebook/milestones/4.2.1>`__.
.. _release-4.2.0:
4.2.0
-----
Release 4.2 adds a new API for enabling and installing extensions.
Extensions can now be enabled at the system-level, rather than just per-user.
An API is defined for installing directly from a Python package, as well.
.. seealso::
:doc:`./examples/Notebook/Distributing Jupyter Extensions as Python Packages`
Highlighted changes:
- Upgrade MathJax to 2.6 to fix vertical-bar appearing on some equations.
- Restore ability for notebook directory to be root (4.1 regression)
- Large outputs are now throttled, reducing the ability of output floods to
kill the browser.
- Fix the notebook ignoring cell executions while a kernel is starting by queueing the messages.
- Fix handling of url prefixes (e.g. JupyterHub) in terminal and edit pages.
- Support nested SVGs in output.
And various other fixes and improvements.
.. _release-4.1.0:
4.1.0

@ -0,0 +1,74 @@
.. _configuration-overview:
Configuration Overview
======================
Beyond the default configuration settings, you can configure a rich array of
options to suit your workflow. Here are areas that are commonly configured
when using Jupyter Notebook:
- :ref:`Jupyter's common configuration system <configure_common>`
- :ref:`Notebook server <configure_nbserver>`
- :ref:`Notebook front-end client <configure_nbclient>`
- :ref:`Notebook extensions <configure_nbextensions>`
Let's look at highlights of each area.
.. _configure_common:
Jupyter's Common Configuration system
-------------------------------------
Jupyter applications, from the Notebook to JupyterHub to nbgrader, share a
common configuration system. The process for creating a configuration file
and editing settings is similar for all the Jupyter applications.
- `Configuring a Jupyter application <https://jupyter.readthedocs.org/en/latest/config.html#configuring-jupyter-applications>`_
- `Using Python to set up the configuration files <https://jupyter.readthedocs.org/en/latest/config.html#python-config-files>`_
- `Configuring a language kernel <http://jupyter.readthedocs.org/en/latest/install.html#installing-kernels>`_
- `traitlets <https://traitlets.readthedocs.org/en/latest/config.html#module-traitlets.config>`_ provide a low-level
architecture for configuration.
.. _configure_nbserver:
Notebook server
---------------
The Notebook server runs the language kernel and communicates with the
front-end Notebook client (i.e. the familiar notebook interface).
- Configuring the Notebook server
To create a ``jupyter_notebook_config.py`` file in the ``.jupyter``
directory, with all the defaults commented out, use the following
command::
$ jupyter notebook --generate-config
:ref:`Command line arguments for configuration <config>` settings are
documented in the configuration file and the user documentation.
- :ref:`Running a Notebook server <working_remotely>`
- Related: `Configuring a language kernel <http://jupyter.readthedocs.org/en/latest/install.html#installing-kernels>`_
to run in the Notebook server enables your server to run other languages, like R or Julia.
.. _configure_nbclient:
Notebook front-end client
-------------------------
- :ref:`How front-end configuration works <frontend_config>`
* :ref:`Example: Changing the notebook's default indentation setting <frontend_config>`
* :ref:`Example: Restoring the notebook's default indentation setting <frontend_config>`
- :ref:`Persisting configuration settings <frontend_config>`
.. _configure_nbextensions:
Notebook extensions
-------------------
- `Distributing Jupyter Extensions as Python Packages <https://jupyter-notebook.readthedocs.org/en/latest/examples/Notebook/Distributing%20Jupyter%20Extensions%20as%20Python%20Packages.html#Distributing-Jupyter-Extensions-as-Python-Packages>`_
- `Extending the Notebook <https://jupyter-notebook.readthedocs.org/en/latest/extending/index.html>`_
:ref:`Security in Jupyter notebooks: <notebook_security>` Since security
policies vary from organization to organization, we encourage you to
consult with your security team on settings that would be best for your use
cases. Our documentation offers some responsible security practices, and we
recommend becoming familiar with the practices.

@ -159,6 +159,7 @@
"source": [
"### Defining the server extension\n",
"This example shows that the server extension and its `load_jupyter_server_extension` function are defined in the `__init__.py` file.\n",
"\n",
"#### `my_module/__init__.py`\n",
"\n",
"```python\n",

@ -40,7 +40,7 @@
"outputs": [],
"source": [
"from IPython import get_ipython\n",
"from IPython.nbformat import current\n",
"from nbformat import read\n",
"from IPython.core.interactiveshell import InteractiveShell"
]
},
@ -130,7 +130,7 @@
" \n",
" # load the notebook object\n",
" with io.open(path, 'r', encoding='utf-8') as f:\n",
" nb = current.read(f, 'json')\n",
" nb = read(f, 4)\n",
" \n",
" \n",
" # create the module and add it to sys.modules\n",
@ -148,10 +148,10 @@
" self.shell.user_ns = mod.__dict__\n",
" \n",
" try:\n",
" for cell in nb.worksheets[0].cells:\n",
" if cell.cell_type == 'code' and cell.language == 'python':\n",
" for cell in nb.cells:\n",
" if cell.cell_type == 'code':\n",
" # transform the input to executable Python\n",
" code = self.shell.input_transformer_manager.transform_cell(cell.input)\n",
" code = self.shell.input_transformer_manager.transform_cell(cell.source)\n",
" # run the code in themodule\n",
" exec(code, mod.__dict__)\n",
" finally:\n",
@ -261,7 +261,18 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"So I should be able to `import nbimp.mynotebook`.\n"
"So I should be able to `import nbpackage.mynotebook`."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"import nbpackage.mynotebook"
]
},
{
@ -316,12 +327,12 @@
"def show_notebook(fname):\n",
" \"\"\"display a short summary of the cells of a notebook\"\"\"\n",
" with io.open(fname, 'r', encoding='utf-8') as f:\n",
" nb = current.read(f, 'json')\n",
" nb = read(f, 4)\n",
" html = []\n",
" for cell in nb.worksheets[0].cells:\n",
" for cell in nb.cells:\n",
" html.append(\"<h4>%s cell</h4>\" % cell.cell_type)\n",
" if cell.cell_type == 'code':\n",
" html.append(highlight(cell.input, lexer, formatter))\n",
" html.append(highlight(cell.source, lexer, formatter))\n",
" else:\n",
" html.append(\"<pre>%s</pre>\" % cell.source)\n",
" display(HTML('\\n'.join(html)))\n",
@ -463,7 +474,7 @@
"outputs": [],
"source": [
"import shutil\n",
"from IPython.utils.path import get_ipython_package_dir\n",
"from IPython.paths import get_ipython_package_dir\n",
"\n",
"utils = os.path.join(get_ipython_package_dir(), 'utils')\n",
"shutil.copy(os.path.join(\"nbpackage\", \"mynotebook.ipynb\"),\n",
@ -515,7 +526,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.5.1"
"version": "3.5.1+"
}
},
"nbformat": 4,

@ -61,7 +61,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.4.2"
"version": "3.5.1+"
}
},
"nbformat": 4,

@ -229,3 +229,49 @@ extension without uninstalling it.
.. versionchanged:: 4.2
Added ``--sys-prefix`` argument
Kernel Specific extensions
--------------------------
.. warning::
This feature serves as a stopgap for kernel developers who need specific
JavaScript injected onto the page. The availability and API are subject to
change at anytime.
It is possible to load some JavaScript on the page on a per kernel basis. Be
aware that doing so will make the browser page reload without warning as
soon as the user switches the kernel without notice.
If you, a kernel developer, need a particular piece of JavaScript to be loaded
on a "per kernel" basis, such as:
* if you are developing a CodeMirror mode for your language
* if you need to enable some specific debugging options
your ``kernelspecs`` are allowed to contain a ``kernel.js`` file that defines
an AMD module. The AMD module should define an `onload` function that will be
called when the kernelspec loads, such as:
* when you load a notebook that uses your kernelspec
* change the active kernelspec of a notebook to your kernelspec.
Note that adding a `kernel.js` to your kernelspec will add an unexpected side
effect to changing a kernel in the notebook. As it is impossible to "unload"
JavaScript, any attempt to change the kernelspec again will save the current
notebook and reload the page without confirmations.
Here is an example of ``kernel.js``::
.. code:: javascript
// kernel.js
define(function(){
return {onload: function(){
console.info('Kernel specific javascript loaded');
// do more things here, like define a codemirror mode,
}}
});

@ -2,22 +2,6 @@
The Jupyter notebook
====================
.. sidebar:: What's New in Jupyter Notebook
:subtitle: Release :ref:`release-4.1.0`
`Release Announcement <https://blog.jupyter.org/2016/01/08/notebook-4-1-release/>`_
- Cell toolbar selector moved to View menu
- Restart & Run All Cells added to Kernel menu
- Multiple-cell selection and actions including cut, copy, paste and execute
- Command palette added for executing Jupyter actions
- Find and replace added to Edit menu
To upgrade to the release:
``pip install notebook --upgrade``
or
``conda upgrade notebook``
.. toctree::
:maxdepth: 1
:caption: User Documentation
@ -32,13 +16,14 @@ The Jupyter notebook
:maxdepth: 2
:caption: Configuration
config_overview
config
public_server
security
frontend_config
examples/Notebook/Distributing Jupyter Extensions as Python Packages
extending/index.rst
.. toctree::
:maxdepth: 1
:caption: Contributor Documentation

@ -43,6 +43,7 @@ using the following command::
$ jupyter notebook --generate-config
.. _hashed-pw:
Preparing a hashed password
~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -103,14 +104,12 @@ with the command::
When starting the notebook server, your browser may warn that your self-signed
certificate is insecure or unrecognized. If you wish to have a fully
compliant self-signed certificate that will not raise warnings, it is possible
(but rather involved) to create one, as explained in detail in `this tutorial`__.
.. __: http://arstechnica.com/security/news/2009/12/how-to-get-set-with-a-secure-sertificate-for-free.ars
.. TODO: Find an additional resource that walks the user through this two-process step by step.
(but rather involved) to create one, as explained in detail in this `tutorial`_.
Alternatively, you may use `Let's Encrypt`_ to acquire a free SSL certificate
and follow the steps in :ref:`using-lets-encrypt` to set up a public server.
.. _OWASP: https://www.owasp.org
.. _tutorial: http://arstechnica.com/security/news/2009/12/how-to-get-set-with-a-secure-sertificate-for-free.ars
.. _notebook_public_server:
@ -134,7 +133,7 @@ config file for the notebook using the following command line::
In the ``~/.jupyter`` directory, edit the notebook config file,
``jupyter_notebook_config.py``. By default, the notebook config file has
all fields commented out. The minimum set of configuration options that
you should to uncomment and edit in :file:``jupyter_notebook_config.py`` is the
you should to uncomment and edit in :file:`jupyter_notebook_config.py` is the
following::
# Set options for certfile, ip, password, and toggle off browser auto-opening
@ -150,6 +149,44 @@ following::
You can then start the notebook using the ``jupyter notebook`` command.
.. _using-lets-encrypt:
Using Let's Encrypt
~~~~~~~~~~~~~~~~~~~
`Let's Encrypt`_ provides free SSL/TLS certificates. You can also set up a
public server using a `Let's Encrypt`_ certificate.
:ref:`notebook_public_server` will be similar when using a Let's Encrypt
certificate with a few configuration changes. Here are the steps:
1. Create a `Let's Encrypt certificate <https://letsencrypt.org/getting-started/>`_.
2. Use :ref:`hashed-pw` to create one.
3. If you don't already have config file for the notebook, create one
using the following command:
.. code-block:: bash
$ jupyter notebook --generate-config
4. In the ``~/.jupyter`` directory, edit the notebook config file,
``jupyter_notebook_config.py``. By default, the notebook config file has
all fields commented out. The minimum set of configuration options that
you should to uncomment and edit in :file:`jupyter_notebook_config.py` is the
following::
# Set options for certfile, ip, password, and toggle off browser auto-opening
c.NotebookApp.certfile = u'/absolute/path/to/your/certificate/fullchain.pem'
c.NotebookApp.keyfile = u'/absolute/path/to/your/certificate/privkey.pem'
# Set ip to '*' to bind on all interfaces (ips) for the public server
c.NotebookApp.ip = '*'
c.NotebookApp.password = u'sha1:bcd259ccf...<your hashed password here>'
c.NotebookApp.open_browser = False
# It is a good idea to set a known, fixed port for server access
c.NotebookApp.port = 9999
You can then start the notebook using the ``jupyter notebook`` command.
.. important::
**Use 'https'.**
@ -165,13 +202,16 @@ You may now access the public server by pointing your browser to
``https://your.host.com:9999`` where ``your.host.com`` is your public server's
domain.
.. _`Let's Encrypt`: https://letsencrypt.org
Firewall Setup
~~~~~~~~~~~~~~
To function correctly, the firewall on the computer running the jupyter
notebook server must be configured to allow connections from client
machines on the access port ``c.NotebookApp.port`` set in
:file:``jupyter_notebook_config.py`` port to allow connections to the
:file:`jupyter_notebook_config.py` port to allow connections to the
web interface. The firewall must also allow connections from
127.0.0.1 (localhost) on ports from 49152 to 65535.
These ports are used by the server to communicate with the notebook kernels.

@ -45,15 +45,13 @@ the question "Did the current user do this?"
This signature is a digest of the notebooks contents plus a secret key,
known only to the user. The secret key is a user-only readable file in
the Jupyter profile's security directory. By default, this is::
the Jupyter data directory. By default, this is::
~/.jupyter/profile_default/security/notebook_secret
.. note::
~/.local/share/jupyter/notebook_secret # linux
~/Library/Jupyter/notebook_secret # OS X
%APPDATA%/jupyter/notebook_secret # Windows
The notebook secret being stored in the profile means that
loading a notebook in another profile results in it being untrusted,
unless you copy or symlink the notebook secret to share it across profiles.
When a notebook is opened by a user, the server computes a signature
with the user's key, and compares it with the signature stored in the
@ -135,20 +133,13 @@ in an untrusted state. There are three basic approaches to this:
- re-run notebooks when you get them (not always viable)
- explicitly trust notebooks via ``jupyter trust`` or the notebook menu
(annoying, but easy)
- share a notebook secret, and use a Jupyter profile dedicated to the
- share a notebook secret, and use configuration dedicated to the
collaboration while working on the project.
Multiple profiles or machines
*****************************
Since the notebook secret is stored in a profile directory by default,
opening a notebook with a different profile or on a different machine
will result in a different key, and thus be untrusted. The only current
way to address this is by sharing the notebook secret. This can be
facilitated by setting the configurable:
When sharing a notebook secret across configurations, you can use
.. sourcecode:: python
c.NotebookApp.secret_file = "/path/to/notebook_secret"
in each profile, and only sharing the secret once per machine.
to specify a non-default path to the secret file.

@ -3,6 +3,10 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
try:
from urllib.parse import urlparse # Py 3
except ImportError:
from urlparse import urlparse # Py 2
import uuid
from tornado.escape import url_escape
@ -23,13 +27,37 @@ class LoginHandler(IPythonHandler):
message=message,
))
def _redirect_safe(self, url, default=None):
"""Redirect if url is on our PATH
Full-domain redirects are allowed if they pass our CORS origin checks.
Otherwise use default (self.base_url if unspecified).
"""
if default is None:
default = self.base_url
if not url.startswith(self.base_url):
# require that next_url be absolute path within our path
allow = False
# OR pass our cross-origin check
if '://' in url:
# if full URL, run our cross-origin check:
parsed = urlparse(url.lower())
origin = '%s://%s' % (parsed.scheme, parsed.netloc)
if self.allow_origin:
allow = self.allow_origin == origin
elif self.allow_origin_pat:
allow = bool(self.allow_origin_pat.match(origin))
if not allow:
# not allowed, use default
self.log.warn("Not allowing login redirect to %r" % url)
url = default
self.redirect(url)
def get(self):
if self.current_user:
next_url = self.get_argument('next', default=self.base_url)
if not next_url.startswith(self.base_url):
# require that next_url be absolute path within our path
next_url = self.base_url
self.redirect(next_url)
self._redirect_safe(next_url)
else:
self._render()
@ -54,10 +82,7 @@ class LoginHandler(IPythonHandler):
return
next_url = self.get_argument('next', default=self.base_url)
if not next_url.startswith(self.base_url):
# require that next_url be absolute path within our path
next_url = self.base_url
self.redirect(next_url)
self._redirect_safe(next_url)
@classmethod
def get_user(cls, handler):

@ -22,7 +22,7 @@ except ImportError:
from jinja2 import TemplateNotFound
from tornado import web
from tornado import gen
from tornado import gen, escape
from tornado.log import app_log
from notebook._sysinfo import get_sys_info
@ -159,6 +159,10 @@ class IPythonHandler(AuthenticatedHandler):
return url
return url_path_join(self.base_url, url)
@property
def mathjax_config(self):
return self.settings.get('mathjax_config', 'TeX-AMS_HTML-full,Safe')
@property
def base_url(self):
return self.settings.get('base_url', '/')
@ -387,11 +391,10 @@ class APIHandler(IPythonHandler):
self.set_header('Content-Type', 'application/json')
return super(APIHandler, self).finish(*args, **kwargs)
@web.authenticated
def options(self, *args, **kwargs):
self.set_header('Access-Control-Allow-Headers', 'accept, content-type')
self.set_header('Access-Control-Allow-Methods',
'GET, PUT, PATCH, DELETE, OPTIONS')
'GET, PUT, POST, PATCH, DELETE, OPTIONS')
self.finish()
@ -409,7 +412,7 @@ class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
if os.path.splitext(path)[1] == '.ipynb':
name = path.rsplit('/', 1)[-1]
self.set_header('Content-Type', 'application/json')
self.set_header('Content-Disposition','attachment; filename="%s"' % name)
self.set_header('Content-Disposition','attachment; filename="%s"' % escape.url_escape(name))
return web.StaticFileHandler.get(self, path)

@ -12,7 +12,7 @@ except ImportError: #PY2
from base64 import decodestring as decodebytes
from tornado import web
from tornado import web, escape
from notebook.base.handlers import IPythonHandler
@ -39,7 +39,7 @@ class FilesHandler(IPythonHandler):
model = cm.get(path, type='file', content=include_body)
if self.get_argument("download", False):
self.set_header('Content-Disposition','attachment; filename="%s"' % name)
self.set_header('Content-Disposition','attachment; filename="%s"' % escape.url_escape(name))
# get mimetype from filename
if name.endswith('.ipynb'):

@ -1,30 +0,0 @@
"""Tornado handlers for the tree view."""
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
import os
from tornado import web
from ..base.handlers import IPythonHandler, FileFindHandler
class LabHandler(IPythonHandler):
"""Render the Jupyter Lab View."""
@web.authenticated
def get(self):
self.write(self.render_template('lab.html',
page_title='Jupyter Lab',
terminals_available=self.settings['terminals_available'],
mathjax_url=self.mathjax_url))
#-----------------------------------------------------------------------------
# URL to handler mappings
#-----------------------------------------------------------------------------
default_handlers = [
(r"/lab", LabHandler),
(r"/lab/(.*)", FileFindHandler,
{'path': os.path.join(os.path.dirname(__file__), 'build')}),
]

@ -1,29 +0,0 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
'use strict';
require('jupyter-js-plugins/lib/default-theme/index.css');
var phosphide = require('phosphide/lib/core/application');
var app = new phosphide.Application({
extensions: [
require('phosphide/lib/extensions/commandpalette').commandPaletteExtension,
require('jupyter-js-plugins/lib/terminal/plugin').terminalExtension,
require('jupyter-js-plugins/lib/filehandler/plugin').fileHandlerExtension,
require('jupyter-js-plugins/lib/filebrowser/plugin').fileBrowserExtension,
require('jupyter-js-plugins/lib/imagehandler/plugin').imageHandlerExtension,
require('jupyter-js-plugins/lib/help/plugin').helpHandlerExtension,
require('jupyter-js-plugins/lib/notebook/plugin').notebookHandlerExtension,
require('jupyter-js-plugins/lib/shortcuts/plugin').shortcutsExtension,
require('jupyter-js-plugins/lib/about/plugin').aboutExtension
],
providers: [
require('jupyter-js-plugins/lib/documentmanager/plugin').documentManagerProvider,
require('jupyter-js-plugins/lib/services/plugin').servicesProvider
]
});
window.onload = function() {
app.run();
}

@ -1,31 +0,0 @@
{
"private": true,
"name": "jupyter-js-lab",
"version": "0.1.0",
"description": "JupyterLab",
"main": "lib/index.js",
"typings": "lib/index.d.ts",
"dependencies": {
"jupyter-js-plugins": "^0.11.8",
"phosphide": "^0.9.0"
},
"devDependencies": {
"css-loader": "^0.23.1",
"es6-promise": "^3.0.2",
"file-loader": "^0.8.5",
"json-loader": "^0.5.4",
"rimraf": "^2.5.0",
"style-loader": "^0.13.0",
"typescript": "^1.7.5",
"url-loader": "^0.5.7",
"webpack": "^1.12.11"
},
"scripts": {
"clean": "rimraf build",
"build": "npm update jupyter-js-plugins && webpack --config webpack.conf.js",
"postinstall": "npm dedupe",
"test": "echo 'no tests specified'"
},
"author": "Project Jupyter",
"license": "BSD-3-Clause"
}

@ -1,37 +0,0 @@
// Support for Node 0.10
// See https://github.com/webpack/css-loader/issues/144
require('es6-promise').polyfill();
module.exports = {
entry: './index.js',
output: {
path: __dirname + "/build",
filename: "bundle.js",
publicPath: "lab/"
},
node: {
fs: "empty"
},
debug: true,
bail: true,
module: {
loaders: [
{ test: /\.css$/, loader: 'style-loader!css-loader' },
{ test: /\.json$/, loader: 'json-loader' },
{ test: /\.html$/, loader: 'file'},
// jquery-ui loads some images
{ test: /\.(jpg|png|gif)$/, loader: "file" },
// required to load font-awesome
{ test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&minetype=application/font-woff" },
{ test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&minetype=application/font-woff" },
{ test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&minetype=application/octet-stream" },
{ test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, loader: "file" },
{ test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, loader: "url?limit=10000&minetype=image/svg+xml" }
]
},
externals: {
"base/js/namespace": "base/js/namespace",
"notebook/js/outputarea": "notebook/js/outputarea",
"services/kernels/comm": "services/kernels/comm"
}
}

@ -7,7 +7,8 @@ import io
import os
import zipfile
from tornado import web
from tornado import web, escape
from tornado.log import app_log
from ..base.handlers import (
IPythonHandler, FilesRedirectHandler,
@ -38,7 +39,7 @@ def respond_zip(handler, name, output, resources):
# Headers
zip_filename = os.path.splitext(name)[0] + '.zip'
handler.set_header('Content-Disposition',
'attachment; filename="%s"' % zip_filename)
'attachment; filename="%s"' % escape.url_escape(zip_filename))
handler.set_header('Content-Type', 'application/zip')
# Prepare the zip file
@ -70,6 +71,7 @@ def get_exporter(format, **kwargs):
try:
return Exporter(**kwargs)
except Exception as e:
app_log.exception("Could not construct Exporter: %s", Exporter)
raise web.HTTPError(500, "Could not construct Exporter: %s" % e)
class NbconvertFileHandler(IPythonHandler):
@ -103,6 +105,7 @@ class NbconvertFileHandler(IPythonHandler):
}
)
except Exception as e:
self.log.exception("nbconvert failed: %s", e)
raise web.HTTPError(500, "nbconvert failed: %s" % e)
if respond_zip(self, name, output, resources):
@ -112,7 +115,7 @@ class NbconvertFileHandler(IPythonHandler):
if self.get_argument('download', 'false').lower() == 'true':
filename = os.path.splitext(name)[0] + resources['output_extension']
self.set_header('Content-Disposition',
'attachment; filename="%s"' % filename)
'attachment; filename="%s"' % escape.url_escape(filename))
# MIME type
if exporter.output_mimetype:

@ -58,8 +58,17 @@ class APITest(NotebookTestBase):
nbdir = self.notebook_dir.name
if not os.path.isdir(pjoin(nbdir, 'foo')):
os.mkdir(pjoin(nbdir, 'foo'))
subdir = pjoin(nbdir, 'foo')
os.mkdir(subdir)
# Make sure that we clean this up when we're done.
# By using addCleanup this will happen correctly even if we fail
# later in setUp.
@self.addCleanup
def cleanup_dir():
shutil.rmtree(subdir, ignore_errors=True)
nb = new_notebook()
nb.cells.append(new_markdown_cell(u'Created by test ³'))
@ -77,12 +86,6 @@ class APITest(NotebookTestBase):
self.nbconvert_api = NbconvertAPI(self.base_url())
def tearDown(self):
nbdir = self.notebook_dir.name
for dname in ['foo']:
shutil.rmtree(pjoin(nbdir, dname), ignore_errors=True)
@onlyif_cmds_exist('pandoc')
def test_from_file(self):
r = self.nbconvert_api.from_file('html', 'foo', 'testnb.ipynb')

@ -686,7 +686,8 @@ class InstallNBExtensionApp(BaseNBExtensionApp):
symlink = Bool(False, config=True, help="Create symlinks instead of copying files")
prefix = Unicode('', config=True, help="Installation prefix")
nbextensions_dir = Unicode('', config=True, help="Full path to nbextensions dir (probably use prefix or user)")
nbextensions_dir = Unicode('', config=True,
help="Full path to nbextensions dir (probably use prefix or user)")
destination = Unicode('', config=True, help="Destination for the copy or symlink")
def _config_file_name_default(self):
@ -696,9 +697,15 @@ class InstallNBExtensionApp(BaseNBExtensionApp):
def install_extensions(self):
"""Perform the installation of nbextension(s)"""
if len(self.extra_args)>1:
raise ValueError("only one nbextension allowed at a time. Call multiple times to install multiple extensions.")
install = install_nbextension_python if self.python else install_nbextension
raise ValueError("Only one nbextension allowed at a time. "
"Call multiple times to install multiple extensions.")
if self.python:
install = install_nbextension_python
kwargs = {}
else:
install = install_nbextension
kwargs = {'destination': self.destination}
full_dests = install(self.extra_args[0],
overwrite=self.overwrite,
@ -707,7 +714,9 @@ class InstallNBExtensionApp(BaseNBExtensionApp):
sys_prefix=self.sys_prefix,
prefix=self.prefix,
nbextensions_dir=self.nbextensions_dir,
logger=self.log)
logger=self.log,
**kwargs
)
if full_dests:
self.log.info(
@ -1042,8 +1051,17 @@ def _get_nbextension_dir(user=False, sys_prefix=False, prefix=None, nbextensions
nbextensions_dir : str [optional]
Get what you put in
"""
if sum(map(bool, [user, prefix, nbextensions_dir, sys_prefix])) > 1:
raise ArgumentConflict("cannot specify more than one of user, sys_prefix, prefix, or nbextensions_dir")
conflicting = [
('user', user),
('prefix', prefix),
('nbextensions_dir', nbextensions_dir),
('sys_prefix', sys_prefix),
]
conflicting_set = ['{}={!r}'.format(n, v) for n, v in conflicting if v]
if len(conflicting_set) > 1:
raise ArgumentConflict(
"cannot specify more than one of user, sys_prefix, prefix, or nbextensions_dir, but got: {}"
.format(', '.join(conflicting_set)))
if user:
nbext = pjoin(jupyter_data_dir(), u'nbextensions')
elif sys_prefix:
@ -1113,7 +1131,8 @@ def _get_nbextension_metadata(module):
"""
m = import_item(module)
if not hasattr(m, '_jupyter_nbextension_paths'):
raise KeyError('The Python module {} is not a valid nbextension'.format(module))
raise KeyError('The Python module {} is not a valid nbextension, '
'it is missing the `_jupyter_nbextension_paths()` method.'.format(module))
nbexts = m._jupyter_nbextension_paths()
return m, nbexts

@ -40,6 +40,7 @@ class NotebookHandler(IPythonHandler):
notebook_name=name,
kill_kernel=False,
mathjax_url=self.mathjax_url,
mathjax_config=self.mathjax_config
)
)

@ -12,6 +12,7 @@ import importlib
import io
import json
import logging
import mimetypes
import os
import random
import re
@ -77,7 +78,7 @@ from .services.sessions.sessionmanager import SessionManager
from .auth.login import LoginHandler
from .auth.logout import LogoutHandler
from .base.handlers import FileFindHandler, IPythonHandler
from .base.handlers import FileFindHandler
from traitlets.config import Config
from traitlets.config.application import catch_config_error, boolean_flag
@ -90,7 +91,7 @@ from jupyter_client.session import Session
from nbformat.sign import NotebookNotary
from traitlets import (
Dict, Unicode, Integer, List, Bool, Bytes, Instance,
TraitError, Type, Float
TraitError, Type, Float, observe, default, validate
)
from ipython_genutils import py3compat
from jupyter_core.paths import jupyter_runtime_dir, jupyter_path
@ -143,7 +144,7 @@ def load_handlers(name):
class NotebookWebApplication(web.Application):
def __init__(self, ipython_app, kernel_manager, contents_manager,
def __init__(self, jupyter_app, kernel_manager, contents_manager,
session_manager, kernel_spec_manager,
config_manager, log,
base_url, default_url, settings_overrides, jinja_env_options):
@ -156,14 +157,14 @@ class NotebookWebApplication(web.Application):
log.info(DEV_NOTE_NPM)
settings = self.init_settings(
ipython_app, kernel_manager, contents_manager,
jupyter_app, kernel_manager, contents_manager,
session_manager, kernel_spec_manager, config_manager, log, base_url,
default_url, settings_overrides, jinja_env_options)
handlers = self.init_handlers(settings)
super(NotebookWebApplication, self).__init__(handlers, **settings)
def init_settings(self, ipython_app, kernel_manager, contents_manager,
def init_settings(self, jupyter_app, kernel_manager, contents_manager,
session_manager, kernel_spec_manager,
config_manager,
log, base_url, default_url, settings_overrides,
@ -171,7 +172,7 @@ class NotebookWebApplication(web.Application):
_template_path = settings_overrides.get(
"template_path",
ipython_app.template_file_path,
jupyter_app.template_file_path,
)
if isinstance(_template_path, py3compat.string_types):
_template_path = (_template_path,)
@ -190,7 +191,7 @@ class NotebookWebApplication(web.Application):
# reset the cache on server restart
version_hash = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
if ipython_app.ignore_minified_js:
if jupyter_app.ignore_minified_js:
log.warn("""The `ignore_minified_js` flag is deprecated and no
longer works. Alternatively use `npm run build:watch` when
working on the notebook's Javascript and LESS""")
@ -202,8 +203,8 @@ class NotebookWebApplication(web.Application):
base_url=base_url,
default_url=default_url,
template_path=template_path,
static_path=ipython_app.static_file_path,
static_custom_path=ipython_app.static_custom_path,
static_path=jupyter_app.static_file_path,
static_custom_path=jupyter_app.static_custom_path,
static_handler_class = FileFindHandler,
static_url_prefix = url_path_join(base_url,'/static/'),
static_handler_args = {
@ -211,19 +212,19 @@ class NotebookWebApplication(web.Application):
'no_cache_paths': [url_path_join(base_url, 'static', 'custom')],
},
version_hash=version_hash,
ignore_minified_js=ipython_app.ignore_minified_js,
ignore_minified_js=jupyter_app.ignore_minified_js,
# rate limits
iopub_msg_rate_limit=ipython_app.iopub_msg_rate_limit,
iopub_data_rate_limit=ipython_app.iopub_data_rate_limit,
rate_limit_window=ipython_app.rate_limit_window,
iopub_msg_rate_limit=jupyter_app.iopub_msg_rate_limit,
iopub_data_rate_limit=jupyter_app.iopub_data_rate_limit,
rate_limit_window=jupyter_app.rate_limit_window,
# authentication
cookie_secret=ipython_app.cookie_secret,
cookie_secret=jupyter_app.cookie_secret,
login_url=url_path_join(base_url,'/login'),
login_handler_class=ipython_app.login_handler_class,
logout_handler_class=ipython_app.logout_handler_class,
password=ipython_app.password,
login_handler_class=jupyter_app.login_handler_class,
logout_handler_class=jupyter_app.logout_handler_class,
password=jupyter_app.password,
# managers
kernel_manager=kernel_manager,
@ -233,12 +234,13 @@ class NotebookWebApplication(web.Application):
config_manager=config_manager,
# IPython stuff
jinja_template_vars=ipython_app.jinja_template_vars,
nbextensions_path=ipython_app.nbextensions_path,
websocket_url=ipython_app.websocket_url,
mathjax_url=ipython_app.mathjax_url,
config=ipython_app.config,
config_dir=ipython_app.config_dir,
jinja_template_vars=jupyter_app.jinja_template_vars,
nbextensions_path=jupyter_app.nbextensions_path,
websocket_url=jupyter_app.websocket_url,
mathjax_url=jupyter_app.mathjax_url,
mathjax_config=jupyter_app.mathjax_config,
config=jupyter_app.config,
config_dir=jupyter_app.config_dir,
jinja2_env=env,
terminals_available=False, # Set later if terminals are available
)
@ -268,7 +270,6 @@ class NotebookWebApplication(web.Application):
handlers.extend(load_handlers('services.nbconvert.handlers'))
handlers.extend(load_handlers('services.kernelspecs.handlers'))
handlers.extend(load_handlers('services.security.handlers'))
handlers.extend(load_handlers('lab.handlers'))
# BEGIN HARDCODED WIDGETS HACK
# TODO: Remove on notebook 5.0
@ -425,14 +426,17 @@ class NotebookApp(JupyterApp):
_log_formatter_cls = LogFormatter
def _log_level_default(self):
@default('log_level')
def _default_log_level(self):
return logging.INFO
def _log_datefmt_default(self):
@default('log_datefmt')
def _default_log_datefmt(self):
"""Exclude date from default date format"""
return "%H:%M:%S"
def _log_format_default(self):
@default('log_format')
def _default_log_format(self):
"""override default log format to include time"""
return u"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"
@ -483,7 +487,9 @@ class NotebookApp(JupyterApp):
ip = Unicode('localhost', config=True,
help="The IP address the notebook server will listen on."
)
def _ip_default(self):
@default('ip')
def _default_ip(self):
"""Return localhost if available, 127.0.0.1 otherwise.
On some (horribly broken) systems, localhost cannot be bound.
@ -498,12 +504,17 @@ class NotebookApp(JupyterApp):
s.close()
return 'localhost'
def _ip_changed(self, name, old, new):
if new == u'*': self.ip = u''
@validate('ip')
def _valdate_ip(self, proposal):
value = proposal['value']
if value == u'*':
value = u''
return value
port = Integer(8888, config=True,
help="The port the notebook server will listen on."
)
port_retries = Integer(50, config=True,
help="The number of additional ports to try if the specified port is not available."
)
@ -517,13 +528,15 @@ class NotebookApp(JupyterApp):
)
client_ca = Unicode(u'', config=True,
help="""The full path to a certificate authority certifificate for SSL/TLS client authentication."""
help="""The full path to a certificate authority certificate for SSL/TLS client authentication."""
)
cookie_secret_file = Unicode(config=True,
help="""The file where the cookie secret is stored."""
)
def _cookie_secret_file_default(self):
@default('cookie_secret_file')
def _default_cookie_secret_file(self):
return os.path.join(self.runtime_dir, 'notebook_cookie_secret')
cookie_secret = Bytes(b'', config=True,
@ -535,7 +548,9 @@ class NotebookApp(JupyterApp):
cookie_secret stored in plaintext (you can read the value from a file).
"""
)
def _cookie_secret_default(self):
@default('cookie_secret')
def _default_cookie_secret(self):
if os.path.exists(self.cookie_secret_file):
with io.open(self.cookie_secret_file, 'rb') as f:
return f.read()
@ -598,14 +613,19 @@ class NotebookApp(JupyterApp):
webapp_settings = Dict(config=True,
help="DEPRECATED, use tornado_settings"
)
def _webapp_settings_changed(self, name, old, new):
@observe('webapp_settings')
def _update_webapp_settings(self, change):
self.log.warning("\n webapp_settings is deprecated, use tornado_settings.\n")
self.tornado_settings = new
self.tornado_settings = change['new']
tornado_settings = Dict(config=True,
help="Supply overrides for the tornado.web.Application that the "
"Jupyter notebook uses.")
terminado_settings = Dict(config=True,
help='Supply overrides for terminado. Currently only supports "shell_command".')
cookie_options = Dict(config=True,
help="Extra keyword arguments to pass to `set_secure_cookie`."
" See tornado's set_secure_cookie docs for details."
@ -632,9 +652,11 @@ class NotebookApp(JupyterApp):
When disabled, equations etc. will appear as their untransformed TeX source.
"""
)
def _enable_mathjax_changed(self, name, old, new):
@observe('enable_mathjax')
def _update_enable_mathjax(self, change):
"""set mathjax url to empty if mathjax is disabled"""
if not new:
if not change['new']:
self.mathjax_url = u''
base_url = Unicode('/', config=True,
@ -643,16 +665,22 @@ class NotebookApp(JupyterApp):
Leading and trailing slashes can be omitted,
and will automatically be added.
''')
def _base_url_changed(self, name, old, new):
if not new.startswith('/'):
self.base_url = '/'+new
elif not new.endswith('/'):
self.base_url = new+'/'
@validate('base_url')
def _update_base_url(self, proposal):
value = proposal['value']
if not value.startswith('/'):
value = '/' + value
elif not value.endswith('/'):
value = value + '/'
return value
base_project_url = Unicode('/', config=True, help="""DEPRECATED use base_url""")
def _base_project_url_changed(self, name, old, new):
@observe('base_project_url')
def _update_base_project_url(self, change):
self.log.warning("base_project_url is deprecated, use base_url")
self.base_url = new
self.base_url = change['new']
extra_static_paths = List(Unicode(), config=True,
help="""Extra paths to search for serving static files.
@ -669,7 +697,9 @@ class NotebookApp(JupyterApp):
static_custom_path = List(Unicode(),
help="""Path to search for custom.js, css"""
)
def _static_custom_path_default(self):
@default('static_custom_path')
def _default_static_custom_path(self):
return [
os.path.join(d, 'custom') for d in (
self.config_dir,
@ -711,33 +741,48 @@ class NotebookApp(JupyterApp):
Should be in the form of an HTTP origin: ws[s]://hostname[:port]
"""
)
mathjax_url = Unicode("", config=True,
help="""The url for MathJax.js."""
)
def _mathjax_url_default(self):
@default('mathjax_url')
def _default_mathjax_url(self):
if not self.enable_mathjax:
return u''
static_url_prefix = self.tornado_settings.get("static_url_prefix", "static")
return url_path_join(static_url_prefix, 'components', 'MathJax', 'MathJax.js')
def _mathjax_url_changed(self, name, old, new):
@observe('mathjax_url')
def _update_mathjax_url(self, change):
new = change['new']
if new and not self.enable_mathjax:
# enable_mathjax=False overrides mathjax_url
self.mathjax_url = u''
else:
self.log.info("Using MathJax: %s", new)
mathjax_config = Unicode("TeX-AMS_HTML-full,Safe", config=True,
help="""The MathJax.js configuration file that is to be used."""
)
@observe('mathjax_config')
def _update_mathjax_config(self, change):
self.log.info("Using MathJax configuration file: %s", change['new'])
contents_manager_class = Type(
default_value=FileContentsManager,
klass=ContentsManager,
config=True,
help='The notebook manager class to use.'
)
kernel_manager_class = Type(
default_value=MappingKernelManager,
config=True,
help='The kernel manager class to use.'
)
session_manager_class = Type(
default_value=SessionManager,
config=True,
@ -785,7 +830,8 @@ class NotebookApp(JupyterApp):
info_file = Unicode()
def _info_file_default(self):
@default('info_file')
def _default_info_file(self):
info_file = "nbserver-%s.json" % os.getpid()
return os.path.join(self.runtime_dir, info_file)
@ -794,10 +840,12 @@ class NotebookApp(JupyterApp):
DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.
"""
)
def _pylab_changed(self, name, old, new):
@observe('pylab')
def _update_pylab(self, change):
"""when --pylab is specified, display a warning and exit"""
if new != 'warn':
backend = ' %s' % new
if change['new'] != 'warn':
backend = ' %s' % change['new']
else:
backend = ''
self.log.error("Support for specifying --pylab on the command line has been removed.")
@ -810,21 +858,22 @@ class NotebookApp(JupyterApp):
help="The directory to use for notebooks and kernels."
)
def _notebook_dir_default(self):
@default('notebook_dir')
def _default_notebook_dir(self):
if self.file_to_run:
return os.path.dirname(os.path.abspath(self.file_to_run))
else:
return py3compat.getcwd()
def _notebook_dir_validate(self, value, trait):
@validate('notebook_dir')
def _notebook_dir_validate(self, proposal):
value = proposal['value']
# Strip any trailing slashes
# *except* if it's root
_, path = os.path.splitdrive(value)
if path == os.sep:
return value
value = value.rstrip(os.sep)
if not os.path.isabs(value):
# If we receive a non-absolute path, make it absolute.
value = os.path.abspath(value)
@ -832,9 +881,11 @@ class NotebookApp(JupyterApp):
raise TraitError("No such notebook dir: %r" % value)
return value
def _notebook_dir_changed(self, name, old, new):
@observe('notebook_dir')
def _update_notebook_dir(self, change):
"""Do a bit of validation of the notebook dir."""
# setting App.notebook_dir implies setting notebook and kernel dirs as well
new = change['new']
self.config.FileContentsManager.root_dir = new
self.config.MappingKernelManager.root_dir = new
@ -842,9 +893,11 @@ class NotebookApp(JupyterApp):
server_extensions = List(Unicode(), config=True,
help=("DEPRECATED use the nbserver_extensions dict instead")
)
def _server_extensions_changed(self, name, old, new):
@observe('server_extensions')
def _update_server_extensions(self, change):
self.log.warning("server_extensions is deprecated, use nbserver_extensions")
self.server_extensions = new
self.server_extensions = change['new']
nbserver_extensions = Dict({}, config=True,
help=("Dict of Python modules to load as notebook server extensions."
@ -967,10 +1020,10 @@ class NotebookApp(JupyterApp):
else:
# SSL may be missing, so only import it if it's to be used
import ssl
# Disable SSLv3, since its use is discouraged.
ssl_options['ssl_version'] = ssl.PROTOCOL_TLSv1
# Disable SSLv3 by default, since its use is discouraged.
ssl_options.setdefault('ssl_version', ssl.PROTOCOL_TLSv1)
if ssl_options.get('ca_certs', False):
ssl_options['cert_reqs'] = ssl.CERT_REQUIRED
ssl_options.setdefault('cert_reqs', ssl.CERT_REQUIRED)
self.login_handler_class.validate_security(self, ssl_options=ssl_options)
self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
@ -1015,7 +1068,7 @@ class NotebookApp(JupyterApp):
def init_terminals(self):
try:
from .terminal import initialize
initialize(self.web_app, self.notebook_dir, self.connection_url)
initialize(self.web_app, self.notebook_dir, self.connection_url, self.terminado_settings)
self.web_app.settings['terminals_available'] = True
except ImportError as e:
log = self.log.debug if sys.platform == 'win32' else self.log.warning
@ -1115,7 +1168,14 @@ class NotebookApp(JupyterApp):
raise
self.log.warning("Error loading server extension %s", modulename,
exc_info=True)
def init_mime_overrides(self):
# On some Windows machines, an application has registered an incorrect
# mimetype for CSS in the registry. Tornado uses this when serving
# .css files, causing browsers to reject the stylesheet. We know the
# mimetype always needs to be text/css, so we override it here.
mimetypes.add_type('text/css', '.css')
@catch_config_error
def initialize(self, argv=None):
super(NotebookApp, self).initialize(argv)
@ -1128,6 +1188,7 @@ class NotebookApp(JupyterApp):
self.init_terminals()
self.init_signal()
self.init_server_extensions()
self.init_mime_overrides()
def cleanup_kernels(self):
"""Shutdown all kernels.
@ -1211,7 +1272,8 @@ class NotebookApp(JupyterApp):
relpath = os.path.relpath(self.file_to_run, self.notebook_dir)
uri = url_escape(url_path_join('notebooks', *relpath.split(os.sep)))
else:
uri = self.default_url
# default_url contains base_url, but so does connection_url
uri = self.default_url[len(self.base_url):]
if browser:
b = lambda : browser.open(url_path_join(self.connection_url, uri),
new=2)

@ -7,7 +7,7 @@ import os.path
from traitlets.config.manager import BaseJSONConfigManager, recursive_update
from jupyter_core.paths import jupyter_config_dir, jupyter_config_path
from traitlets import Unicode, Instance, List
from traitlets import Unicode, Instance, List, observe, default
from traitlets.config import LoggingConfigurable
@ -19,11 +19,12 @@ class ConfigManager(LoggingConfigurable):
def get(self, section_name):
"""Get the config from all config sections."""
config = {}
for p in self.read_config_path:
# step through back to front, to ensure front of the list is top priority
for p in self.read_config_path[::-1]:
cm = BaseJSONConfigManager(config_dir=p)
recursive_update(config, cm.get(section_name))
return config
def set(self, section_name, data):
"""Set the config only to the user's config."""
return self.write_config_manager.set(section_name, data)
@ -35,16 +36,23 @@ class ConfigManager(LoggingConfigurable):
# Private API
read_config_path = List(Unicode())
def _read_config_path_default(self):
@default('read_config_path')
def _default_read_config_path(self):
return [os.path.join(p, 'nbconfig') for p in jupyter_config_path()]
write_config_dir = Unicode()
def _write_config_dir_default(self):
@default('write_config_dir')
def _default_write_config_dir(self):
return os.path.join(jupyter_config_dir(), 'nbconfig')
write_config_manager = Instance(BaseJSONConfigManager)
def _write_config_manager_default(self):
@default('write_config_manager')
def _default_write_config_manager(self):
return BaseJSONConfigManager(config_dir=self.write_config_dir)
def _write_config_dir_changed(self, name, old, new):
@observe('write_config_dir')
def _update_write_config_dir(self, change):
self.write_config_manager = BaseJSONConfigManager(config_dir=self.write_config_dir)

@ -30,6 +30,19 @@ except ImportError: #PY2
from base64 import encodestring as encodebytes, decodestring as decodebytes
def replace_file(src, dst):
""" replace dst with src
switches between os.replace or os.rename based on python 2.7 or python 3
"""
if hasattr(os, 'replace'): # PY3
os.replace(src, dst)
else:
if os.name == 'nt' and os.path.exists(dst):
# Rename over existing file doesn't work on Windows
os.remove(dst)
os.rename(src, dst)
def copy2_safe(src, dst, log=None):
"""copy src to dst
@ -42,6 +55,17 @@ def copy2_safe(src, dst, log=None):
if log:
log.debug("copystat on %s failed", dst, exc_info=True)
def path_to_intermediate(path):
'''Name of the intermediate file used in atomic writes.
The .~ prefix will make Dropbox ignore the temporary file.'''
dirname, basename = os.path.split(path)
return os.path.join(dirname, '.~'+basename)
def path_to_invalid(path):
'''Name of invalid file after a failed atomic write and subsequent read.'''
dirname, basename = os.path.split(path)
return os.path.join(dirname, basename+'.invalid')
@contextmanager
def atomic_writing(path, text=True, encoding='utf-8', log=None, **kwargs):
@ -73,9 +97,8 @@ def atomic_writing(path, text=True, encoding='utf-8', log=None, **kwargs):
if os.path.islink(path):
path = os.path.join(os.path.dirname(path), os.readlink(path))
dirname, basename = os.path.split(path)
# The .~ prefix will make Dropbox ignore the temporary file.
tmp_path = os.path.join(dirname, '.~'+basename)
tmp_path = path_to_intermediate(path)
if os.path.isfile(path):
copy2_safe(path, tmp_path, log=log)
@ -91,10 +114,7 @@ def atomic_writing(path, text=True, encoding='utf-8', log=None, **kwargs):
except:
# Failed! Move the backup file back to the real path to avoid corruption
fileobj.close()
if os.name == 'nt' and os.path.exists(path):
# Rename over existing file doesn't work on Windows
os.remove(path)
os.rename(tmp_path, path)
replace_file(tmp_path, path)
raise
# Flush to disk
@ -249,11 +269,25 @@ class FileManagerMixin(Configurable):
try:
return nbformat.read(f, as_version=as_version)
except Exception as e:
e_orig = e
# If use_atomic_writing is enabled, we'll guess that it was also
# enabled when this notebook was written and look for a valid
# atomic intermediate.
tmp_path = path_to_intermediate(os_path)
if not self.use_atomic_writing or not os.path.exists(tmp_path):
raise HTTPError(
400,
u"Unreadable Notebook: %s %r" % (os_path, e),
u"Unreadable Notebook: %s %r" % (os_path, e_orig),
)
# Move the bad file aside, restore the intermediate, and try again.
invalid_file = path_to_invalid(os_path)
replace_file(os_path, invalid_file)
replace_file(tmp_path, os_path)
return self._read_notebook(os_path, as_version)
def _save_notebook(self, os_path, nb):
"""Save a notebook to an os_path."""
with self.atomic_writing(os_path, encoding='utf-8') as f:

@ -17,9 +17,8 @@ from .filecheckpoints import FileCheckpoints
from .fileio import FileManagerMixin
from .manager import ContentsManager
from ipython_genutils.importstring import import_item
from traitlets import Any, Unicode, Bool, TraitError
from traitlets import Any, Unicode, Bool, TraitError, observe, default, validate
from ipython_genutils.py3compat import getcwd, string_types
from . import tz
from notebook.utils import (
@ -59,14 +58,17 @@ class FileContentsManager(FileManagerMixin, ContentsManager):
root_dir = Unicode(config=True)
def _root_dir_default(self):
@default('root_dir')
def _default_root_dir(self):
try:
return self.parent.notebook_dir
except AttributeError:
return getcwd()
save_script = Bool(False, config=True, help='DEPRECATED, use post_save_hook. Will be removed in Notebook 5.0')
def _save_script_changed(self):
@observe('save_script')
def _update_save_script(self):
self.log.warning("""
`--script` is deprecated and will be removed in notebook 5.0.
@ -101,12 +103,15 @@ class FileContentsManager(FileManagerMixin, ContentsManager):
- contents_manager: this ContentsManager instance
"""
)
def _post_save_hook_changed(self, name, old, new):
if new and isinstance(new, string_types):
self.post_save_hook = import_item(self.post_save_hook)
elif new:
if not callable(new):
raise TraitError("post_save_hook must be callable")
@validate('post_save_hook')
def _validate_post_save_hook(self, proposal):
value = proposal['value']
if isinstance(value, string_types):
value = import_item(value)
if not callable(value):
raise TraitError("post_save_hook must be callable")
return value
def run_post_save_hook(self, model, os_path):
"""Run the post-save hook if defined, and log errors"""
@ -114,17 +119,20 @@ class FileContentsManager(FileManagerMixin, ContentsManager):
try:
self.log.debug("Running post-save hook on %s", os_path)
self.post_save_hook(os_path=os_path, model=model, contents_manager=self)
except Exception:
self.log.error("Post-save hook failed on %s", os_path, exc_info=True)
except Exception as e:
self.log.error("Post-save hook failed o-n %s", os_path, exc_info=True)
raise web.HTTPError(500, u'Unexpected error while running post hook save: %s' % e)
def _root_dir_changed(self, name, old, new):
@validate('root_dir')
def _validate_root_dir(self, proposal):
"""Do a bit of validation of the root_dir."""
if not os.path.isabs(new):
value = proposal['value']
if not os.path.isabs(value):
# If we receive a non-absolute path, make it absolute.
self.root_dir = os.path.abspath(new)
return
if not os.path.isdir(new):
raise TraitError("%r is not a directory" % new)
value = os.path.abspath(value)
if not os.path.isdir(value):
raise TraitError("%r is not a directory" % value)
return value
def _checkpoints_class_default(self):
return FileCheckpoints

@ -13,7 +13,7 @@ from tornado.web import HTTPError
from .checkpoints import Checkpoints
from traitlets.config.configurable import LoggingConfigurable
from nbformat import sign, validate, ValidationError
from nbformat import sign, validate as validate_nb, ValidationError
from nbformat.v4 import new_notebook
from ipython_genutils.importstring import import_item
from traitlets import (
@ -24,6 +24,8 @@ from traitlets import (
TraitError,
Type,
Unicode,
validate,
default,
)
from ipython_genutils.py3compat import string_types
@ -91,12 +93,15 @@ class ContentsManager(LoggingConfigurable):
- contents_manager: this ContentsManager instance
"""
)
def _pre_save_hook_changed(self, name, old, new):
if new and isinstance(new, string_types):
self.pre_save_hook = import_item(self.pre_save_hook)
elif new:
if not callable(new):
raise TraitError("pre_save_hook must be callable")
@validate('pre_save_hook')
def _validate_pre_save_hook(self, proposal):
value = proposal['value']
if isinstance(value, string_types):
value = import_item(self.pre_save_hook)
if not callable(value):
raise TraitError("pre_save_hook must be callable")
return value
def run_pre_save_hook(self, model, path, **kwargs):
"""Run the pre-save hook if defined, and log errors"""
@ -111,10 +116,12 @@ class ContentsManager(LoggingConfigurable):
checkpoints = Instance(Checkpoints, config=True)
checkpoints_kwargs = Dict(config=True)
def _checkpoints_default(self):
@default('checkpoints')
def _default_checkpoints(self):
return self.checkpoints_class(**self.checkpoints_kwargs)
def _checkpoints_kwargs_default(self):
@default('checkpoints_kwargs')
def _default_checkpoints_kwargs(self):
return dict(
parent=self,
log=self.log,
@ -292,9 +299,9 @@ class ContentsManager(LoggingConfigurable):
def validate_notebook_model(self, model):
"""Add failed-validation message to model"""
try:
validate(model['content'])
validate_nb(model['content'])
except ValidationError as e:
model['message'] = u'Notebook Validation failed: {}:\n{}'.format(
model['message'] = u'Notebook validation failed: {}:\n{}'.format(
e.message, json.dumps(e.instance, indent=1, default=lambda obj: '<UNKNOWN>'),
)
return model

@ -2,6 +2,7 @@
"""Test the contents webservice API."""
from contextlib import contextmanager
from functools import partial
import io
import json
import os
@ -195,32 +196,32 @@ class APITest(NotebookTestBase):
def isdir(self, api_path):
return os.path.isdir(self.to_os_path(api_path))
def setUp(self):
def setUp(self):
for d in (self.dirs + self.hidden_dirs):
self.make_dir(d)
self.addCleanup(partial(self.delete_dir, d))
for d, name in self.dirs_nbs:
# create a notebook
nb = new_notebook()
self.make_nb(u'{}/{}.ipynb'.format(d, name), nb)
nbname = u'{}/{}.ipynb'.format(d, name)
self.make_nb(nbname, nb)
self.addCleanup(partial(self.delete_file, nbname))
# create a text file
txt = self._txt_for_name(name)
self.make_txt(u'{}/{}.txt'.format(d, name), txt)
# create a binary file
txtname = u'{}/{}.txt'.format(d, name)
self.make_txt(txtname, txt)
self.addCleanup(partial(self.delete_file, txtname))
blob = self._blob_for_name(name)
self.make_blob(u'{}/{}.blob'.format(d, name), blob)
blobname = u'{}/{}.blob'.format(d, name)
self.make_blob(blobname, blob)
self.addCleanup(partial(self.delete_file, blobname))
self.api = API(self.base_url())
def tearDown(self):
for dname in (list(self.top_level_dirs) + self.hidden_dirs):
self.delete_dir(dname)
self.delete_file('inroot.ipynb')
def test_list_notebooks(self):
nbs = notebooks_only(self.api.list().json())
self.assertEqual(len(nbs), 1)
@ -291,6 +292,24 @@ class APITest(NotebookTestBase):
self.assertIn('content', nb)
self.assertEqual(nb['content'], None)
def test_get_nb_invalid(self):
nb = {
'nbformat': 4,
'metadata': {},
'cells': [{
'cell_type': 'wrong',
'metadata': {},
}],
}
path = u'å b/Validate tést.ipynb'
self.make_txt(path, py3compat.cast_unicode(json.dumps(nb)))
model = self.api.read(path).json()
self.assertEqual(model['path'], path)
self.assertEqual(model['type'], 'notebook')
self.assertIn('content', model)
self.assertIn('message', model)
self.assertIn("validation failed", model['message'].lower())
def test_get_contents_no_such_file(self):
# Name that doesn't exist - should be a 404
with assert_http_error(404):

@ -16,7 +16,7 @@ from jupyter_client.jsonutil import date_default
from ipython_genutils.py3compat import cast_unicode
from notebook.utils import url_path_join, url_escape
from ...base.handlers import IPythonHandler, APIHandler, json_errors
from ...base.handlers import APIHandler, json_errors
from ...base.zmqhandlers import AuthenticatedZMQStreamHandler, deserialize_binary_message
from jupyter_client import protocol_version as client_protocol_version
@ -96,6 +96,11 @@ class KernelActionHandler(APIHandler):
class ZMQChannelsHandler(AuthenticatedZMQStreamHandler):
# class-level registry of open sessions
# allows checking for conflict on session-id,
# which is used as a zmq identity and must be unique.
_open_sessions = {}
@property
def kernel_info_timeout(self):
@ -194,6 +199,8 @@ class ZMQChannelsHandler(AuthenticatedZMQStreamHandler):
self.kernel_id = None
self.kernel_info_channel = None
self._kernel_info_future = Future()
self._close_future = Future()
self.session_key = ''
# Rate limiting code
self._iopub_window_msg_count = 0
@ -209,6 +216,8 @@ class ZMQChannelsHandler(AuthenticatedZMQStreamHandler):
def pre_get(self):
# authenticate first
super(ZMQChannelsHandler, self).pre_get()
# check session collision:
yield self._register_session()
# then request kernel info, waiting up to a certain time before giving up.
# We don't want to wait forever, because browsers don't take it well when
# servers never respond to websocket connection requests.
@ -232,6 +241,21 @@ class ZMQChannelsHandler(AuthenticatedZMQStreamHandler):
self.kernel_id = cast_unicode(kernel_id, 'ascii')
yield super(ZMQChannelsHandler, self).get(kernel_id=kernel_id)
@gen.coroutine
def _register_session(self):
"""Ensure we aren't creating a duplicate session.
If a previous identical session is still open, close it to avoid collisions.
This is likely due to a client reconnecting from a lost network connection,
where the socket on our side has not been cleaned up yet.
"""
self.session_key = '%s:%s' % (self.kernel_id, self.session.session)
stale_handler = self._open_sessions.get(self.session_key)
if stale_handler:
self.log.warning("Replacing stale connection: %s", self.session_key)
yield stale_handler.close()
self._open_sessions[self.session_key] = self
def open(self, kernel_id):
super(ZMQChannelsHandler, self).open()
try:
@ -348,8 +372,15 @@ class ZMQChannelsHandler(AuthenticatedZMQStreamHandler):
return
super(ZMQChannelsHandler, self)._on_zmq_reply(stream, msg)
def close(self):
super(ZMQChannelsHandler, self).close()
return self._close_future
def on_close(self):
self.log.debug("Websocket closed %s", self.session_key)
# unregister myself as an open session (only if it's really me)
if self._open_sessions.get(self.session_key) is self:
self._open_sessions.pop(self.session_key)
km = self.kernel_manager
if self.kernel_id in km:
km.remove_restart_callback(
@ -370,6 +401,7 @@ class ZMQChannelsHandler(AuthenticatedZMQStreamHandler):
socket.close()
self.channels = {}
self._close_future.set_result(None)
def _send_status_message(self, status):
msg = self.session.msg("status",

@ -14,7 +14,7 @@ from tornado.concurrent import Future
from tornado.ioloop import IOLoop
from jupyter_client.multikernelmanager import MultiKernelManager
from traitlets import List, Unicode, TraitError
from traitlets import List, Unicode, TraitError, default, validate
from notebook.utils import to_os_path
from ipython_genutils.py3compat import getcwd
@ -23,27 +23,31 @@ from ipython_genutils.py3compat import getcwd
class MappingKernelManager(MultiKernelManager):
"""A KernelManager that handles notebook mapping and HTTP error handling"""
def _kernel_manager_class_default(self):
@default('kernel_manager_class')
def _default_kernel_manager_class(self):
return "jupyter_client.ioloop.IOLoopKernelManager"
kernel_argv = List(Unicode())
root_dir = Unicode(config=True)
def _root_dir_default(self):
@default('root_dir')
def _default_root_dir(self):
try:
return self.parent.notebook_dir
except AttributeError:
return getcwd()
def _root_dir_changed(self, name, old, new):
@validate('root_dir')
def _update_root_dir(self, proposal):
"""Do a bit of validation of the root dir."""
if not os.path.isabs(new):
value = proposal['value']
if not os.path.isabs(value):
# If we receive a non-absolute path, make it absolute.
self.root_dir = os.path.abspath(new)
return
if not os.path.exists(new) or not os.path.isdir(new):
raise TraitError("kernel root dir %r is not a directory" % new)
value = os.path.abspath(value)
if not os.path.exists(value) or not os.path.isdir(value):
raise TraitError("kernel root dir %r is not a directory" % value)
return value
#-------------------------------------------------------------------------
# Methods for managing kernels and sessions

@ -1,5 +1,6 @@
"""Tests for the session manager."""
from functools import partial
from unittest import TestCase
from tornado import gen, web
@ -39,10 +40,8 @@ class TestSessionManager(TestCase):
contents_manager=ContentsManager(),
)
self.loop = IOLoop()
def tearDown(self):
self.loop.close(all_fds=True)
self.addCleanup(partial(self.loop.close, all_fds=True))
def create_sessions(self, *kwarg_list):
@gen.coroutine
def co_add():

@ -1,6 +1,7 @@
"""Test the sessions web service API."""
import errno
from functools import partial
import io
import os
import json
@ -65,33 +66,34 @@ class SessionAPITest(NotebookTestBase):
"""Test the sessions web service API"""
def setUp(self):
nbdir = self.notebook_dir.name
subdir = pjoin(nbdir, 'foo')
try:
os.mkdir(pjoin(nbdir, 'foo'))
os.mkdir(subdir)
except OSError as e:
# Deleting the folder in an earlier test may have failed
if e.errno != errno.EEXIST:
raise
self.addCleanup(partial(shutil.rmtree, subdir, ignore_errors=True))
with io.open(pjoin(nbdir, 'foo', 'nb1.ipynb'), 'w',
encoding='utf-8') as f:
with io.open(pjoin(subdir, 'nb1.ipynb'), 'w', encoding='utf-8') as f:
nb = new_notebook()
write(nb, f, version=4)
self.sess_api = SessionAPI(self.base_url())
def tearDown(self):
for session in self.sess_api.list().json():
self.sess_api.delete(session['id'])
# This is necessary in some situations on Windows: without it, it
# fails to delete the directory because something is still using it. I
# think there is a brief period after the kernel terminates where
# Windows still treats its working directory as in use. On my Windows
# VM, 0.01s is not long enough, but 0.1s appears to work reliably.
# -- TK, 15 December 2014
time.sleep(0.1)
shutil.rmtree(pjoin(self.notebook_dir.name, 'foo'),
ignore_errors=True)
@self.addCleanup
def cleanup_sessions():
for session in self.sess_api.list().json():
self.sess_api.delete(session['id'])
# This is necessary in some situations on Windows: without it, it
# fails to delete the directory because something is still using
# it. I think there is a brief period after the kernel terminates
# where Windows still treats its working directory as in use. On my
# Windows VM, 0.01s is not long enough, but 0.1s appears to work
# reliably. -- TK, 15 December 2014
time.sleep(0.1)
def test_create(self):
sessions = self.sess_api.list().json()

@ -152,7 +152,7 @@ define(function(require) {
var error_div = $('<div/>').css('color', 'red');
var message =
"Manually edit the JSON below to manipulate the metadata for this " + options.name + "." +
" We recommend putting custom metadata attributes in an appropriately named sub-structure," +
" We recommend putting custom metadata attributes in an appropriately named substructure," +
" so they don't conflict with those of others.";
var textarea = $('<textarea/>')

@ -168,20 +168,27 @@ define([
// Shortcut manager class
var ShortcutManager = function (delay, events, actions, env) {
var ShortcutManager = function (delay, events, actions, env, config, mode) {
/**
* A class to deal with keyboard event and shortcut
*
* @class ShortcutManager
* @constructor
*
* :config: configobjet on which to call `update(....)` to persist the config.
* :mode: mode of this shortcut manager where to persist config.
*/
mode = mode || 'command';
this._shortcuts = {};
this._defaults_bindings = [];
this.delay = delay || 800; // delay in milliseconds
this.events = events;
this.actions = actions;
this.actions.extend_env(env);
this._queue = [];
this._cleartimeout = null;
this._config = config;
this._mode = mode;
Object.seal(this);
};
@ -224,10 +231,21 @@ define([
}
return dct;
};
ShortcutManager.prototype.get_action_shortcuts = function(name){
var ftree = flatten_shorttree(this._shortcuts);
var res = [];
for (var sht in ftree ){
if(ftree[sht] === name){
res.push(sht);
}
}
return res;
};
ShortcutManager.prototype.get_action_shortcut = function(name){
var ftree = flatten_shorttree(this._shortcuts);
var res = {};
for (var sht in ftree ){
if(ftree[sht] === name){
return sht;
@ -334,8 +352,30 @@ define([
}
};
ShortcutManager.prototype.is_available_shortcut = function(shortcut){
var shortcut_array = shortcut.split(',');
return this._is_available_shortcut(shortcut_array, this._shortcuts);
};
ShortcutManager.prototype._is_available_shortcut = function(shortcut_array, tree){
var current_node = tree[shortcut_array[0]];
if(!shortcut_array[0]){
return false;
}
if(current_node === undefined){
return true;
} else {
if (typeof(current_node) == 'string'){
return false;
} else { // assume is a sub-shortcut tree
return this._is_available_shortcut(shortcut_array.slice(1), current_node);
}
}
};
ShortcutManager.prototype._set_leaf = function(shortcut_array, action_name, tree){
var current_node = tree[shortcut_array[0]];
if(shortcut_array.length === 1){
if(current_node !== undefined && typeof(current_node) !== 'string'){
console.warn('[warning], you are overriting a long shortcut with a shorter one');
@ -356,6 +396,52 @@ define([
}
};
ShortcutManager.prototype._persist_shortcut = function(shortcut, data) {
/**
* add a shortcut to this manager and persist it to the config file.
**/
shortcut = shortcut.toLowerCase();
this.add_shortcut(shortcut, data);
var patch = {keys:{}};
var b = {bind:{}};
patch.keys[this._mode] = {bind:{}};
patch.keys[this._mode].bind[shortcut] = data;
this._config.update(patch);
};
ShortcutManager.prototype._persist_remove_shortcut = function(shortcut){
/**
* Remove a shortcut from this manager and persist its removal.
*/
shortcut = shortcut.toLowerCase();
this.remove_shortcut(shortcut);
const patch = {keys:{}};
const b = {bind:{}};
patch.keys[this._mode] = {bind:{}};
patch.keys[this._mode].bind[shortcut] = null;
this._config.update(patch);
// if the shortcut we unbind is a default one, we add it to the list of
// things to unbind at startup
if( this._defaults_bindings.indexOf(shortcut) !== -1 ){
const cnf = (this._config.data.keys||{})[this._mode];
const unbind_array = cnf.unbind||[];
// unless it's already there (like if we have remapped a default
// shortcut to another command): unbind it)
if(unbind_array.indexOf(shortcut) === -1){
const _parray = unbind_array.concat(shortcut);
const unbind_patch = {keys:{}};
unbind_patch.keys[this._mode] = {unbind:_parray}
console.warn('up:', unbind_patch);
this._config.update(unbind_patch);
}
}
};
ShortcutManager.prototype.add_shortcut = function (shortcut, data, suppress_help_update) {
/**
* Add a action to be handled by shortcut manager.
@ -369,8 +455,8 @@ define([
if (! action_name){
throw new Error('does not know how to deal with : ' + data);
}
shortcut = normalize_shortcut(shortcut);
this.set_shortcut(shortcut, action_name);
var _shortcut = normalize_shortcut(shortcut);
this.set_shortcut(_shortcut, action_name);
if (!suppress_help_update) {
// update the keyboard shortcuts notebook help
@ -391,6 +477,16 @@ define([
this.events.trigger('rebuild.QuickHelp');
};
ShortcutManager.prototype._add_default_shortcuts = function (data) {
/**
* same as add_shortcuts, but register them as "default" that if persistently unbound, with
* persist_remove_shortcut, need to be on the "unbind" list.
**/
this._defaults_bindings = this._defaults_bindings.concat(Object.keys(data));
this.add_shortcuts(data);
};
ShortcutManager.prototype.remove_shortcut = function (shortcut, suppress_help_update) {
/**
* Remove the binding of shortcut `sortcut` with its action.
@ -415,7 +511,7 @@ define([
this.events.trigger('rebuild.QuickHelp');
}
} catch (ex) {
throw new Error('trying to remove a non-existent shortcut', shortcut);
throw new Error('trying to remove a non-existent shortcut', shortcut, typeof shortcut);
}
};

@ -453,6 +453,22 @@ define([
return txt;
}
// Remove characters that are overridden by backspace characters
function fixBackspace(txt) {
var tmp = txt;
do {
txt = tmp;
// Cancel out anything-but-newline followed by backspace
tmp = txt.replace(/[^\n]\x08/gm, '');
} while (tmp.length < txt.length);
return txt;
}
// Remove characters overridden by backspace and carriage return
function fixOverwrittenChars(txt) {
return fixCarriageReturn(fixBackspace(txt));
}
// Locate any URLs and convert them to a anchor tag
function autoLinkUrls(txt) {
return txt.replace(/(^|\s)(https?|ftp)(:[^'"<>\s]+)/gi,
@ -946,6 +962,22 @@ define([
}
};
// Test if a drag'n'drop event contains a file (as opposed to an HTML
// element/text from the document)
var dnd_contain_file = function(event) {
// As per the HTML5 drag'n'drop spec, the dataTransfer.types should
// contain one "Files" type if a file is being dragged
// https://www.w3.org/TR/2011/WD-html5-20110113/dnd.html#dom-datatransfer-types
if (event.dataTransfer.types) {
for (var i = 0; i < event.dataTransfer.types.length; i++) {
if (event.dataTransfer.types[i] == "Files") {
return true;
}
}
}
return false;
};
var utils = {
is_loaded: is_loaded,
load_extension: load_extension,
@ -956,6 +988,8 @@ define([
uuid : uuid,
fixConsole : fixConsole,
fixCarriageReturn : fixCarriageReturn,
fixBackspace : fixBackspace,
fixOverwrittenChars: fixOverwrittenChars,
autoLinkUrls : autoLinkUrls,
points_to_pixels : points_to_pixels,
get_body_data : get_body_data,
@ -990,6 +1024,7 @@ define([
time: time,
format_datetime: format_datetime,
datetime_sort_helper: datetime_sort_helper,
dnd_contain_file: dnd_contain_file,
_ansispan:_ansispan
};

@ -25,7 +25,13 @@ require([
notificationarea
){
"use strict";
requirejs(['custom/custom'], function() {});
try {
requirejs(['custom/custom'], function() {});
} catch(err) {
console.log("Error loading custom.js from edition service. Continuing and logging");
console.warn(err);
}
page = new page.Page();

@ -8,19 +8,27 @@ require([
'use strict';
$('#notebook_about').click(function () {
// use underscore template to auto html escape
var text = 'You are using Jupyter notebook.<br/><br/>';
text = text + 'The version of the notebook server is ';
text = text + _.template('<b><%- version %></b>')({ version: sys_info.notebook_version });
if (sys_info.commit_hash) {
text = text + _.template('-<%- hash %>')({ hash: sys_info.commit_hash });
if (sys_info) {
var text = 'You are using Jupyter notebook.<br/><br/>';
text = text + 'The version of the notebook server is ';
text = text + _.template('<b><%- version %></b>')({ version: sys_info.notebook_version });
if (sys_info.commit_hash) {
text = text + _.template('-<%- hash %>')({ hash: sys_info.commit_hash });
}
text = text + _.template(' and is running on:<br/><pre>Python <%- pyver %></pre>')({
pyver: sys_info.sys_version });
var kinfo = $('<div/>').attr('id', '#about-kinfo').text('Waiting for kernel to be available...');
var body = $('<div/>');
body.append($('<h4/>').text('Server Information:'));
body.append($('<p/>').html(text));
body.append($('<h4/>').text('Current Kernel Information:'));
body.append(kinfo);
} else {
var text = 'Could not access sys_info variable for version information.';
var body = $('<div/>');
body.append($('<h4/>').text('Cannot find sys_info!'));
body.append($('<p/>').html(text));
}
text = text + _.template(' and is running on:<br/><pre>Python <%- pyver %></pre>')({ pyver: sys_info.sys_version });
var kinfo = $('<div/>').attr('id', '#about-kinfo').text('Waiting for kernel to be available...');
var body = $('<div/>');
body.append($('<h4/>').text('Server Information:'));
body.append($('<p/>').html(text));
body.append($('<h4/>').text('Current Kernel Information:'));
body.append(kinfo);
dialog.modal({
title: 'About Jupyter Notebook',
body: body,

@ -62,6 +62,12 @@ define(function(require){
*
**/
var _actions = {
'edit-command-mode-keyboard-shortcuts': {
help: 'Open a dialog to edit the command mode keyboard shortcuts',
handler: function (env) {
env.notebook.show_shortcuts_editor();
}
},
'restart-kernel': {
help: 'restart the kernel (no confirmation dialog)',
handler: function (env) {

@ -33,7 +33,9 @@ function load_json(clipboard) {
}
function copy(event) {
if (Jupyter.notebook.mode !== 'command') {
if ((Jupyter.notebook.mode !== 'command') ||
// window.getSelection checks if text is selected, e.g. in output
!window.getSelection().isCollapsed) {
return;
}
var selecn = Jupyter.notebook.get_selected_cells().map(
@ -66,6 +68,18 @@ function paste(event) {
event.preventDefault();
}
function notebookOnlyEvent(callback) {
// Only call the callback to redirect the event if the notebook should be
// handling the events, at the descretion of the keyboard manager.
// If the focus is in a text widget or something (kbmanager disabled),
// allow the default event.
return function() {
if (Jupyter.keyboard_manager.enabled) {
callback.apply(this, arguments);
}
};
}
function needs_text_box_for_paste_event() {
// I know this is bad, but I don't know a better way to check this
return navigator.userAgent.indexOf('Firefox') !== -1;
@ -122,11 +136,11 @@ function setup_paste_dialog() {
// Set clipboard event listeners on the document.
return {setup_clipboard_events: function() {
document.addEventListener('copy', copy);
document.addEventListener('copy', notebookOnlyEvent(copy));
if (needs_text_box_for_paste_event()) {
setup_paste_dialog();
} else {
document.addEventListener('paste', paste);
document.addEventListener('paste', notebookOnlyEvent(paste));
}
}};
});

@ -5,34 +5,35 @@
(function(mod) {
if (typeof exports == "object" && typeof module == "object"){ // CommonJS
mod(require("codemirror/lib/codemirror"),
require("codemirror/mode/python/python")
mod(require("codemirror/lib/codemirror")
);
} else if (typeof define == "function" && define.amd){ // AMD
define(["codemirror/lib/codemirror",
"codemirror/mode/python/python"], mod);
define(["codemirror/lib/codemirror"], mod);
} else {// Plain browser env
mod(CodeMirror);
}
})(function(CodeMirror) {
"use strict";
CodeMirror.defineMode("ipython", function(conf, parserConf) {
var pythonConf = {};
for (var prop in parserConf) {
if (parserConf.hasOwnProperty(prop)) {
pythonConf[prop] = parserConf[prop];
requirejs(['codemirror/mode/python/python'], function () {
CodeMirror.defineMode("ipython", function(conf, parserConf) {
var pythonConf = {};
for (var prop in parserConf) {
if (parserConf.hasOwnProperty(prop)) {
pythonConf[prop] = parserConf[prop];
}
}
}
pythonConf.name = 'python';
pythonConf.singleOperators = new RegExp("^[\\+\\-\\*/%&|\\^~<>!\\?]");
if (pythonConf.version === 3) {
pythonConf.identifiers = new RegExp("^[_A-Za-z\u00A1-\uFFFF][_A-Za-z0-9\u00A1-\uFFFF]*");
} else if (pythonConf.version === 2) {
pythonConf.identifiers = new RegExp("^[_A-Za-z][_A-Za-z0-9]*");
}
return CodeMirror.getMode(conf, pythonConf);
}, 'python');
pythonConf.name = 'python';
pythonConf.singleOperators = new RegExp("^[\\+\\-\\*/%&|\\^~<>!\\?]");
if (pythonConf.version === 3) {
pythonConf.identifiers = new RegExp("^[_A-Za-z\u00A1-\uFFFF][_A-Za-z0-9\u00A1-\uFFFF]*");
} else if (pythonConf.version === 2) {
pythonConf.identifiers = new RegExp("^[_A-Za-z][_A-Za-z0-9]*");
}
return CodeMirror.getMode(conf, pythonConf);
}, 'python');
CodeMirror.defineMIME("text/x-ipython", "ipython");
CodeMirror.defineMIME("text/x-ipython", "ipython");
});
})

@ -10,15 +10,9 @@
(function(mod) {
if (typeof exports == "object" && typeof module == "object"){ // CommonJS
mod(require("codemirror/lib/codemirror")
,require("codemirror/addon/mode/multiplex")
,require("codemirror/mode/gfm/gfm")
,require("codemirror/mode/stex/stex")
);
} else if (typeof define == "function" && define.amd){ // AMD
define(["codemirror/lib/codemirror"
,"codemirror/addon/mode/multiplex"
,"codemirror/mode/python/python"
,"codemirror/mode/stex/stex"
], mod);
} else {// Plain browser env
mod(CodeMirror);
@ -26,37 +20,40 @@
})( function(CodeMirror){
"use strict";
CodeMirror.defineMode("ipythongfm", function(config, parserConfig) {
requirejs(["codemirror/addon/mode/multiplex", "codemirror/mode/gfm/gfm", "codemirror/mode/stex/stex"], function () {
var gfm_mode = CodeMirror.getMode(config, "gfm");
var tex_mode = CodeMirror.getMode(config, "stex");
CodeMirror.defineMode("ipythongfm", function(config, parserConfig) {
return CodeMirror.multiplexingMode(
gfm_mode,
{
open: "$", close: "$",
mode: tex_mode,
delimStyle: "delimit"
},
{
// not sure this works as $$ is interpreted at (opening $, closing $, as defined just above)
open: "$$", close: "$$",
mode: tex_mode,
delimStyle: "delimit"
},
{
open: "\\(", close: "\\)",
mode: tex_mode,
delimStyle: "delimit"
},
{
open: "\\[", close: "\\]",
mode: tex_mode,
delimStyle: "delimit"
}
// .. more multiplexed styles can follow here
);
}, 'gfm');
var gfm_mode = CodeMirror.getMode(config, "gfm");
var tex_mode = CodeMirror.getMode(config, "stex");
return CodeMirror.multiplexingMode(
gfm_mode,
{
open: "$", close: "$",
mode: tex_mode,
delimStyle: "delimit"
},
{
// not sure this works as $$ is interpreted at (opening $, closing $, as defined just above)
open: "$$", close: "$$",
mode: tex_mode,
delimStyle: "delimit"
},
{
open: "\\(", close: "\\)",
mode: tex_mode,
delimStyle: "delimit"
},
{
open: "\\[", close: "\\]",
mode: tex_mode,
delimStyle: "delimit"
}
// .. more multiplexed styles can follow here
);
}, 'gfm');
CodeMirror.defineMIME("text/x-ipythongfm", "ipythongfm");
CodeMirror.defineMIME("text/x-ipythongfm", "ipythongfm");
});
})

@ -253,10 +253,8 @@ define([
//build the container
var that = this;
this.sel.dblclick(function () {
this.sel.click(function () {
that.pick();
});
this.sel.focus(function () {
that.editor.focus();
});
this._handle_keydown = function (cm, event) {

@ -131,6 +131,7 @@ define([
$("#kernel_indicator").find('.kernel_indicator_name').text(ks.spec.display_name);
if (ks.resources['logo-64x64']) {
logo_img.attr("src", ks.resources['logo-64x64']);
logo_img.attr("title", ks.spec.display_name);
logo_img.show();
} else {
logo_img.hide();

@ -36,12 +36,12 @@ define([
this.bind_events();
this.env = {pager:this.pager};
this.actions = options.actions;
this.command_shortcuts = new keyboard.ShortcutManager(undefined, options.events, this.actions, this.env );
this.command_shortcuts.add_shortcuts(this.get_default_common_shortcuts());
this.command_shortcuts.add_shortcuts(this.get_default_command_shortcuts());
this.command_shortcuts = new keyboard.ShortcutManager(undefined, options.events, this.actions, this.env, options.config, 'command');
this.command_shortcuts._add_default_shortcuts(this.get_default_common_shortcuts());
this.command_shortcuts._add_default_shortcuts(this.get_default_command_shortcuts());
this.edit_shortcuts = new keyboard.ShortcutManager(undefined, options.events, this.actions, this.env);
this.edit_shortcuts.add_shortcuts(this.get_default_common_shortcuts());
this.edit_shortcuts.add_shortcuts(this.get_default_edit_shortcuts());
this.edit_shortcuts._add_default_shortcuts(this.get_default_common_shortcuts());
this.edit_shortcuts._add_default_shortcuts(this.get_default_edit_shortcuts());
this.config = options.config;
@ -106,7 +106,7 @@ define([
'ctrl-enter' : 'jupyter-notebook:run-cell',
'alt-enter' : 'jupyter-notebook:run-cell-and-insert-below',
// cmd on mac, ctrl otherwise
'cmdtrl-s' : 'jupyter-notebook:save-notebook',
'cmdtrl-s' : 'jupyter-notebook:save-notebook'
};
};

@ -2,6 +2,25 @@
// Distributed under the terms of the Modified BSD License.
__webpack_public_path__ = window['staticURL'] + 'notebook/js/built/';
// adapted from Mozilla Developer Network example at
// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/bind
// shim `bind` for testing under casper.js
var bind = function bind(obj) {
var slice = [].slice;
var args = slice.call(arguments, 1),
self = this,
nop = function() {
},
bound = function() {
return self.apply(this instanceof nop ? this : (obj || {}), args.concat(slice.call(arguments)));
};
nop.prototype = this.prototype || {}; // Firefox cries sometimes if prototype is undefined
bound.prototype = new nop();
return bound;
};
Function.prototype.bind = Function.prototype.bind || bind ;
requirejs(['contents'], function(contentsModule) {
require([
'base/js/namespace',
@ -52,8 +71,13 @@ require([
// Pull typeahead from the global jquery object
var typeahead = $.typeahead;
requirejs(['custom/custom'], function() {});
try{
requirejs(['custom/custom'], function() {});
} catch(err) {
console.log("Error processing custom.js. Logging and continuing")
console.warn(err);
}
// compat with old IPython, remove for IPython > 3.0
window.CodeMirror = CodeMirror;
@ -192,7 +216,7 @@ require([
})
// BEGIN HARDCODED WIDGETS HACK
.then(function() {
if (!utils.is_loaded('widgets/extension')) {
if (!utils.is_loaded('jupyter-js-widgets/extension')) {
// Fallback to the ipywidgets extension
utils.load_extension('widgets/notebook/js/extension').catch(function () {
console.warn('Widgets are not available. Please install widgetsnbextension or ipywidgets 4.0');

@ -4,31 +4,31 @@
/**
* @module notebook
*/
define(function (require) {
"use strict";
var IPython = require('base/js/namespace');
var _ = require('underscore');
var utils = require('base/js/utils');
var dialog = require('base/js/dialog');
var cellmod = require('notebook/js/cell');
var textcell = require('notebook/js/textcell');
var codecell = require('notebook/js/codecell');
var moment = require('moment');
var configmod = require('services/config');
var session = require('services/sessions/session');
var celltoolbar = require('notebook/js/celltoolbar');
var marked = require('components/marked/lib/marked');
var CodeMirror = require('codemirror/lib/codemirror');
var runMode = require('codemirror/addon/runmode/runmode');
var mathjaxutils = require('notebook/js/mathjaxutils');
var keyboard = require('base/js/keyboard');
var tooltip = require('notebook/js/tooltip');
var default_celltoolbar = require('notebook/js/celltoolbarpresets/default');
var rawcell_celltoolbar = require('notebook/js/celltoolbarpresets/rawcell');
var slideshow_celltoolbar = require('notebook/js/celltoolbarpresets/slideshow');
var attachments_celltoolbar = require('notebook/js/celltoolbarpresets/attachments');
var scrollmanager = require('notebook/js/scrollmanager');
var commandpalette = require('notebook/js/commandpalette');
"use strict";
import IPython from 'base/js/namespace';
import _ from 'underscore';
import utils from 'base/js/utils';
import dialog from 'base/js/dialog';
import cellmod from 'notebook/js/cell';
import textcell from 'notebook/js/textcell';
import codecell from 'notebook/js/codecell';
import moment from 'moment';
import configmod from 'services/config';
import session from 'services/sessions/session';
import celltoolbar from 'notebook/js/celltoolbar';
import marked from 'components/marked/lib/marked';
import CodeMirror from 'codemirror/lib/codemirror';
import runMode from 'codemirror/addon/runmode/runmode';
import mathjaxutils from 'notebook/js/mathjaxutils';
import keyboard from 'base/js/keyboard';
import tooltip from 'notebook/js/tooltip';
import default_celltoolbar from 'notebook/js/celltoolbarpresets/default';
import rawcell_celltoolbar from 'notebook/js/celltoolbarpresets/rawcell';
import slideshow_celltoolbar from 'notebook/js/celltoolbarpresets/slideshow';
import attachments_celltoolbar from 'notebook/js/celltoolbarpresets/attachments';
import scrollmanager from 'notebook/js/scrollmanager';
import commandpalette from 'notebook/js/commandpalette';
import {ShortcutEditor} from 'notebook/js/shortcuteditor';
var _SOFT_SELECTION_CLASS = 'jupyter-soft-selected';
@ -50,7 +50,7 @@ define(function (require) {
* @param {string} options.notebook_path
* @param {string} options.notebook_name
*/
var Notebook = function (selector, options) {
export function Notebook(selector, options) {
this.config = options.config;
this.class_config = new configmod.ConfigWithDefaults(this.config,
Notebook.options_default, 'Notebook');
@ -114,7 +114,8 @@ define(function (require) {
}
}, function (err) {
console.log("No CodeMirror mode: " + lang);
callback(err, code);
console.log("Require CodeMirror mode error: " + err);
callback(null, code);
});
}
});
@ -362,6 +363,10 @@ define(function (require) {
var x = new commandpalette.CommandPalette(this);
};
Notebook.prototype.show_shortcuts_editor = function() {
new ShortcutEditor(this);
};
/**
* Trigger a warning dialog about missing functionality from newer minor versions
*/
@ -3171,5 +3176,3 @@ define(function (require) {
this.load_notebook(this.notebook_path);
};
return {'Notebook': Notebook};
});

@ -24,6 +24,7 @@ define([
NotebookNotificationArea.prototype.init_notification_widgets = function () {
this.init_kernel_notification_widget();
this.init_notebook_notification_widget();
this.init_trusted_notebook_notification_widget();
};
/**
@ -138,8 +139,7 @@ define([
if (info.attempt === 1) {
var msg = "A connection to the notebook server could not be established." +
" The notebook will continue trying to reconnect, but" +
" until it does, you will NOT be able to run code. Check your" +
" The notebook will continue trying to reconnect. Check your" +
" network connection or notebook server configuration.";
dialog.kernel_modal({
@ -339,5 +339,27 @@ define([
});
};
/**
* Initialize the notification widget for trusted notebook messages.
*
* @method init_trusted_notebook_notification_widget
*/
NotebookNotificationArea.prototype.init_trusted_notebook_notification_widget = function () {
var that = this;
var tnw = this.widget('trusted');
// Notebook trust events
this.events.on('trust_changed.Notebook', function (event, trusted) {
if (trusted) {
tnw.set_message("Trusted");
} else {
tnw.set_message("Not Trusted", undefined, function() {
that.notebook.trust_notebook();
return false;
});
}
});
};
return {'NotebookNotificationArea': NotebookNotificationArea};
});

@ -504,7 +504,7 @@ define([
// latest output was in the same stream,
// so append to it instead of making a new output.
// escape ANSI & HTML specials:
last.text = utils.fixCarriageReturn(last.text + json.text);
last.text = utils.fixOverwrittenChars(last.text + json.text);
var pre = this.element.find('div.'+subclass).last().find('pre');
var html = utils.fixConsole(last.text);
html = utils.autoLinkUrls(html);
@ -659,7 +659,7 @@ define([
var append_text = function (data, md, element) {
var type = 'text/plain';
var toinsert = this.create_output_subarea(md, "output_text", type);
data = utils.fixCarriageReturn(data);
data = utils.fixOverwrittenChars(data);
// escape ANSI & HTML specials in plaintext:
data = utils.fixConsole(data);
data = utils.autoLinkUrls(data);
@ -779,7 +779,7 @@ define([
*/
var type = 'text/latex';
var toinsert = this.create_output_subarea(md, "output_latex", type);
toinsert.append(latex);
toinsert.text(latex);
element.append(toinsert);
return toinsert;
};

@ -157,7 +157,7 @@ define([
* The only user content injected with this HTML call is escaped by
* the fixConsole() method.
*/
this.pager_element.find(".container").append($('<pre/>').html(utils.fixConsole(utils.fixCarriageReturn(text))));
this.pager_element.find(".container").append($('<pre/>').html(utils.fixConsole(utils.fixOverwrittenChars(text))));
};
Pager.prototype.append = function (htm) {

@ -153,13 +153,22 @@ define([
return hum;
}
function humanize_shortcut(shortcut){
function _humanize_sequence(sequence){
var joinchar = ',';
var hum = _.map(sequence.replace(/meta/g, 'cmd').split(','), _humanize_shortcut).join(joinchar);
return hum;
}
function _humanize_shortcut(shortcut){
var joinchar = '-';
if (platform === 'MacOS'){
joinchar = '';
}
var sh = _.map(shortcut.split('-'), humanize_key ).join(joinchar);
return '<kbd>'+sh+'</kbd>';
return _.map(shortcut.split('-'), humanize_key ).join(joinchar);
}
function humanize_shortcut(shortcut){
return '<kbd>'+_humanize_shortcut(shortcut)+'</kbd>';
}
@ -301,6 +310,7 @@ define([
return {'QuickHelp': QuickHelp,
humanize_shortcut: humanize_shortcut,
humanize_sequence: humanize_sequence
humanize_sequence: humanize_sequence,
_humanize_sequence: _humanize_sequence,
};
});

@ -0,0 +1,173 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
import QH from "notebook/js/quickhelp";
import dialog from "base/js/dialog";
import {render} from "preact";
import {createElement, createClass} from "preact-compat";
import marked from 'components/marked/lib/marked';
/**
* Humanize the action name to be consumed by user.
* internally the actions name are of the form
* <namespace>:<description-with-dashes>
* we drop <namespace> and replace dashes for space.
*/
const humanize_action_id = function(str) {
return str.split(':')[1].replace(/-/g, ' ').replace(/_/g, '-');
};
/**
* given an action id return 'command-shortcut', 'edit-shortcut' or 'no-shortcut'
* for the action. This allows us to tag UI in order to visually distinguish
* Wether an action have a keybinding or not.
**/
const KeyBinding = createClass({
displayName: 'KeyBindings',
getInitialState: function() {
return {shrt:''};
},
handleShrtChange: function (element){
this.setState({shrt:element.target.value});
},
render: function(){
const that = this;
const available = this.props.available(this.state.shrt);
const empty = (this.state.shrt === '');
return createElement('div', {className:'jupyter-keybindings'},
createElement('i', {className: "pull-right fa fa-plus", alt: 'add-keyboard-shortcut',
onClick:()=>{
available?that.props.onAddBindings(that.state.shrt, that.props.ckey):null;
that.state.shrt='';
}
}),
createElement('input', {
type:'text',
placeholder:'add shortcut',
className:'pull-right'+((available||empty)?'':' alert alert-danger'),
value:this.state.shrt,
onChange:this.handleShrtChange
}),
this.props.shortcuts?this.props.shortcuts.map((item, index) => {
return createElement('span', {className: 'pull-right'},
createElement('kbd', {}, [
item.h,
createElement('i', {className: "fa fa-times", alt: 'remove '+item.h,
onClick:()=>{
that.props.unbind(item.raw);
}
})
])
);
}):null,
createElement('div', {title: '(' +this.props.ckey + ')' , className:'jupyter-keybindings-text'}, this.props.display )
);
}
});
const KeyBindingList = createClass({
displayName: 'KeyBindingList',
getInitialState: function(){
return {data:[]};
},
componentDidMount: function(){
this.setState({data:this.props.callback()});
},
render: function() {
const childrens = this.state.data.map((binding)=>{
return createElement(KeyBinding, Object.assign({}, binding, {onAddBindings:(shortcut, action)=>{
this.props.bind(shortcut, action);
this.setState({data:this.props.callback()});
},
available:this.props.available,
unbind: (shortcut) => {
this.props.unbind(shortcut);
this.setState({data:this.props.callback()});
}
}));
});
childrens.unshift(createElement('div', {className:'well', key:'disclamer', dangerouslySetInnerHTML:
{__html:
marked(
"This dialog allows you to modify the keymap of the command mode, and persist the changes. "+
"You can define many type of shorctuts and sequences of keys. "+
"\n\n"+
" - Use dashes `-` to represent keys that should be pressed with modifiers, "+
"for example `Shift-a`, or `Ctrl-;`. \n"+
" - Separate by commas if the keys need to be pressed in sequence: `h,a,l,t`.\n"+
"\n\nYou can combine the two: `Ctrl-x,Meta-c,Meta-b,u,t,t,e,r,f,l,y`.\n"+
"Casing will have no effects: (e.g: `;` and `:` are the same on english keyboards)."+
" You need to explicitelty write the `Shift` modifier.\n"+
"Valid modifiers are `Cmd`, `Ctrl`, `Alt` ,`Meta`, `Cmdtrl`. Refer to developper docs "+
"for their signification depending on the platform."
)}
}));
return createElement('div',{}, childrens);
}
});
const get_shortcuts_data = function(notebook) {
const actions = Object.keys(notebook.keyboard_manager.actions._actions);
const src = [];
for (let i = 0; i < actions.length; i++) {
const action_id = actions[i];
const action = notebook.keyboard_manager.actions.get(action_id);
let shortcuts = notebook.keyboard_manager.command_shortcuts.get_action_shortcuts(action_id);
let hshortcuts;
if (shortcuts.length > 0) {
hshortcuts = shortcuts.map((raw)=>{
return {h:QH._humanize_sequence(raw),raw:raw};}
);
}
src.push({
display: humanize_action_id(action_id),
shortcuts: hshortcuts,
key:action_id, // react specific thing
ckey: action_id
});
}
return src;
};
export const ShortcutEditor = function(notebook) {
if(!notebook){
throw new Error("CommandPalette takes a notebook non-null mandatory arguement");
}
const body = $('<div>');
const mod = dialog.modal({
notebook: notebook,
keyboard_manager: notebook.keyboard_manager,
title : "Edit Command mode Shortcuts",
body : body,
buttons : {
OK : {}
}
});
const src = get_shortcuts_data(notebook);
mod.addClass("modal_stretch");
mod.modal('show');
render(
createElement(KeyBindingList, {
callback:()=>{ return get_shortcuts_data(notebook);},
bind: (shortcut, command) => {
return notebook.keyboard_manager.command_shortcuts._persist_shortcut(shortcut, command);
},
unbind: (shortcut) => {
return notebook.keyboard_manager.command_shortcuts._persist_remove_shortcut(shortcut);
},
available: (shrt) => { return notebook.keyboard_manager.command_shortcuts.is_available_shortcut(shrt);}
}),
body.get(0)
);
};

@ -25,6 +25,8 @@ define([
ipgfm
) {
"use strict";
function encodeURIandParens(uri){return encodeURI(uri).replace('(','%28').replace(')','%29')}
var Cell = cell.Cell;
var TextCell = function (options) {
@ -110,6 +112,7 @@ define([
inner_cell.append(input_area).append(render_area);
cell.append(inner_cell);
this.element = cell;
this.inner_cell = inner_cell;
};
@ -244,9 +247,9 @@ define([
marked(text, function (err, html) {
html = security.sanitize_html(html);
html = $($.parseHTML(html));
html.find('img[src^="attachment:"]').each(function (i, h) {
html.find('img[src^="attachment://"]').each(function (i, h) {
h = $(h);
var key = h.attr('src').replace(/^attachment:/, '');
var key = h.attr('src').replace(/^attachment:\/\//, '');
if (key in that.attachments) {
data.attachments[key] = JSON.parse(JSON.stringify(
that.attachments[key]));
@ -282,6 +285,9 @@ define([
TextCell.apply(this, [$.extend({}, options, {config: config})]);
this.cell_type = 'markdown';
// Used to keep track of drag events
this.drag_counter = 0;
};
MarkdownCell.options_default = {
@ -340,7 +346,7 @@ define([
// We generate names for blobs
var key;
if (blob.name !== undefined) {
key = blob.name;
key = encodeURIandParens(blob.name);
} else {
key = '_auto_' + Object.keys(that.attachments).length;
}
@ -353,7 +359,7 @@ define([
'type (' + d[0] + ')');
}
that.add_attachment(key, blob.type, d[1]);
var img_md = '![attachment:' + key + '](attachment:' + key + ')';
var img_md = '![' + key + '](attachment://' + key + ')';
that.code_mirror.replaceRange(img_md, pos);
}
reader.readAsDataURL(blob);
@ -363,6 +369,11 @@ define([
* @method render
*/
MarkdownCell.prototype.render = function () {
// We clear the dropzone here just in case the dragenter/leave
// logic of bind_events wasn't 100% successful.
this.drag_counter = 0;
this.inner_cell.removeClass('dropzone');
var cont = TextCell.prototype.render.apply(this);
if (cont) {
var that = this;
@ -395,9 +406,9 @@ define([
html.find("a[href]").not('[href^="#"]').attr("target", "_blank");
// replace attachment:<key> by the corresponding entry
// in the cell's attachments
html.find('img[src^="attachment:"]').each(function (i, h) {
html.find('img[src^="attachment://"]').each(function (i, h) {
h = $(h);
var key = h.attr('src').replace(/^attachment:/, '');
var key = h.attr('src').replace(/^attachment:\/\//, '');
if (key in that.attachments) {
var att = that.attachments[key];
@ -449,19 +460,44 @@ define([
}
});
// Allow drop event if the dragged file can be used as an attachment
this.code_mirror.on("dragstart", function(cm, evt) {
var files = evt.dataTransfer.files;
for (var i = 0; i < files.length; ++i) {
var file = files[i];
if (attachment_regex.test(file.type)) {
return false;
}
// Allow drag event if the dragged file can be used as an attachment
// If we use this.code_mirror.on to register a "dragover" handler, we
// get an empty dataTransfer
this.code_mirror.on("dragover", function(cm, evt) {
if (utils.dnd_contain_file(evt)) {
evt.preventDefault();
}
return true;
});
// We want to display a visual indicator that the drop is possible.
// The dragleave event is fired when we hover a child element (which
// is often immediatly after we got the dragenter), so we keep track
// of the number of dragenter/dragleave we got, as discussed here :
// http://stackoverflow.com/q/7110353/116067
// This doesn't seem to be 100% reliable, so we clear the dropzone
// class when the cell is rendered as well
this.code_mirror.on("dragenter", function(cm, evt) {
if (utils.dnd_contain_file(evt)) {
that.drag_counter++;
that.inner_cell.addClass('dropzone');
}
evt.preventDefault();
evt.stopPropagation();
});
this.code_mirror.on("dragleave", function(cm, evt) {
that.drag_counter--;
if (that.drag_counter <= 0) {
that.inner_cell.removeClass('dropzone');
}
evt.preventDefault();
evt.stopPropagation();
});
this.code_mirror.on("drop", function(cm, evt) {
that.drag_counter = 0;
that.inner_cell.removeClass('dropzone');
var files = evt.dataTransfer.files;
for (var i = 0; i < files.length; ++i) {
var file = files[i];

@ -99,3 +99,19 @@ kbd {
padding-top: 1px;
padding-bottom: 1px;
}
.jupyter-keybindings {
padding: 0px;
line-height: 24px;
border-bottom: 1px solid gray;
}
.jupyter-keybindings input {
margin: 0;
padding: 0;
border: none;
}
.jupyter-keybindings i {
padding: 6px;
}

@ -26,7 +26,7 @@
h6:first-child {margin-top: 1em;}
ul:not(.list-inline),
ol:not(.list-inline) {margin: 0em 2em; padding-left: 0px;}
ol:not(.list-inline) {padding-left: 2em;}
ul {list-style:disc;}
ul ul {list-style:square;}
ul ul ul {list-style:circle;}
@ -92,4 +92,8 @@
max-width: none;
}
}
// Override bootstrap settings, see #1390
.alert {margin-bottom: initial;}
* + .alert {margin-top: 1em;}
}

@ -48,6 +48,11 @@ h1,h2,h3,h4,h5,h6 {
display:none;
}
.text_cell .dropzone .input_area {
border: 2px dashed #bababa;
margin: -1px;
}
.cm-header-1,
.cm-header-2,
.cm-header-3,

@ -338,7 +338,7 @@ define([
* @function reconnect
*/
if (this.is_connected()) {
return;
this.stop_channels();
}
this._reconnect_attempt = this._reconnect_attempt + 1;
this.events.trigger('kernel_reconnecting.Kernel', {
@ -534,8 +534,13 @@ define([
this.events.trigger('kernel_disconnected.Kernel', {kernel: this});
if (error) {
console.log('WebSocket connection failed: ', ws_url);
this.events.trigger('kernel_connection_failed.Kernel', {kernel: this, ws_url: ws_url, attempt: this._reconnect_attempt});
console.log('WebSocket connection failed: ', ws_url, error);
this.events.trigger('kernel_connection_failed.Kernel', {
kernel: this,
ws_url: ws_url,
attempt: this._reconnect_attempt,
error: error,
});
}
this._schedule_reconnect();
};
@ -638,8 +643,8 @@ define([
*/
var msg = this._get_msg(msg_type, content, metadata, buffers);
msg.channel = 'shell';
this._send(serialize.serialize(msg));
this.set_callbacks_for_msg(msg.header.msg_id, callbacks);
this._send(serialize.serialize(msg));
return msg.header.msg_id;
};

@ -1,8 +1,8 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
__webpack_public_path__ = window['staticURL'] + 'terminal/js/built/';
require('xterm/src/xterm.css');
requirejs(['termjs'], function(termjs) {
require([
'base/js/utils',
'base/js/page',
@ -67,4 +67,3 @@ require([
window.terminal = terminal;
});
});

@ -1,4 +1,4 @@
define ([], function() {
define (["xterm"], function(Terminal) {
"use strict";
function make_terminal(element, size, ws_url) {
var ws = new WebSocket(ws_url);

@ -2,6 +2,25 @@
// Distributed under the terms of the Modified BSD License.
__webpack_public_path__ = window['staticURL'] + 'tree/js/built/';
// adapted from Mozilla Developer Network example at
// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/bind
// shim `bind` for testing under casper.js
var bind = function bind(obj) {
var slice = [].slice;
var args = slice.call(arguments, 1),
self = this,
nop = function() {
},
bound = function() {
return self.apply(this instanceof nop ? this : (obj || {}), args.concat(slice.call(arguments)));
};
nop.prototype = this.prototype || {}; // Firefox cries sometimes if prototype is undefined
bound.prototype = new nop();
return bound;
};
Function.prototype.bind = Function.prototype.bind || bind ;
requirejs(['contents'], function(contents_service) {
require([
'base/js/namespace',
@ -30,7 +49,12 @@ require([
newnotebook,
loginwidget){
"use strict";
requirejs(['custom/custom'], function() {});
try{
requirejs(['custom/custom'], function() {});
} catch(err) {
console.log("Error loading custom.js from tree service. Continuing and logging");
console.warn(err);
}
// Setup all of the config related things

@ -52,6 +52,10 @@ define([
'sort-name': 0
};
this._max_upload_size_mb = 25;
this.EDIT_MIMETYPES = [
'application/javascipt',
'application/x-sh',
];
};
NotebookList.prototype.style = function () {
@ -646,9 +650,9 @@ define([
}
var uri_prefix = NotebookList.uri_prefixes[model.type];
if (model.type === 'file' &&
model.mimetype && model.mimetype.substr(0,5) !== 'text/'
&& !model.mimetype.endsWith('javascript')
) {
model.mimetype &&
model.mimetype.substr(0, 5) !== 'text/' &&
this.EDIT_MIMETYPES.indexOf(model.mimetype) < 0) {
// send text/unidentified files to editor, others go to raw viewer
uri_prefix = 'files';
}

@ -1,102 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>{{page_title}}</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<link rel="stylesheet" type="text/css" href="{{static_url("components/font-awesome/css/font-awesome.css")}}"></link>
<script src="{{static_url("components/es6-promise/promise.min.js")}}" type="text/javascript" charset="utf-8"></script>
<script src="{{static_url("components/requirejs/require.js") }}" type="text/javascript" charset="utf-8"></script>
<script>
require.config({
{% if version_hash %}
urlArgs: "v={{version_hash}}",
{% endif %}
baseUrl: '{{static_url("", include_version=False)}}',
paths: {
{% if ignore_minified_js %}
'auth/js/main': 'auth/js/main',
{% else %}
'auth/js/main': 'auth/js/main.min',
{% endif %}
custom : '{{ base_url }}custom',
nbextensions : '{{ base_url }}nbextensions',
kernelspecs : '{{ base_url }}kernelspecs',
underscore : 'components/underscore/underscore-min',
backbone : 'components/backbone/backbone-min',
jquery: 'components/jquery/jquery.min',
bootstrap: 'components/bootstrap/js/bootstrap.min',
bootstraptour: 'components/bootstrap-tour/build/js/bootstrap-tour.min',
'jquery-ui': 'components/jquery-ui/ui/minified/jquery-ui.min',
moment: 'components/moment/moment',
codemirror: 'components/codemirror',
termjs: 'components/term.js/src/term',
typeahead: 'components/jquery-typeahead/dist/jquery.typeahead'
},
map: { // for backward compatibility
"*": {
"jqueryui": "jquery-ui",
}
},
shim: {
typeahead: {
deps: ["jquery"],
exports: "typeahead"
},
underscore: {
exports: '_'
},
backbone: {
deps: ["underscore", "jquery"],
exports: "Backbone"
},
bootstrap: {
deps: ["jquery"],
exports: "bootstrap"
},
bootstraptour: {
deps: ["bootstrap"],
exports: "Tour"
},
"jquery-ui": {
deps: ["jquery"],
exports: "$"
}
},
waitSeconds: 30,
});
require.config({
map: {
'*':{
'contents': '{{ contents_js_source }}',
}
}
});
</script>
{% block meta %}
{% endblock %}
{% if mathjax_url %}
<script type="text/javascript" src="{{mathjax_url}}?config=TeX-AMS_HTML-full,Safe&amp;delayStartupUntil=configured" charset="utf-8"></script>
{% endif %}
</head>
<body class="{% block bodyclasses %}{% endblock %}" {% block params %}{% endblock %}>
{% block script %}
<script id='jupyter-config-data' type="application/json">{
"baseUrl": "{{base_url | urlencode}}",
"wsUrl": "{{ws_url| urlencode}}",
"notebookPath": "{{notebook_path | urlencode}}"
}</script>
<script src="lab/bundle.js" type="text/javascript" charset="utf-8"></script>
{% endblock %}
</body>
</html>

@ -3,7 +3,7 @@
{% block stylesheet %}
{% if mathjax_url %}
<script type="text/javascript" src="{{mathjax_url}}?config=TeX-AMS_HTML-full,Safe&delayStartupUntil=configured" charset="utf-8"></script>
<script type="text/javascript" src="{{mathjax_url}}?config={{mathjax_config}}&delayStartupUntil=configured" charset="utf-8"></script>
{% endif %}
<script type="text/javascript">
// MathJax disabled, set as null to distingish from *missing* MathJax,

@ -20,7 +20,9 @@
<script src="{{static_url("components/jquery-ui/ui/minified/jquery-ui.min.js") }}" type="text/javascript" charset="utf-8"></script> <!-- extends window.$ -->
<script src="{{static_url("components/bootstrap/js/bootstrap.min.js") }}" type="text/javascript" charset="utf-8"></script> <!-- extends window.$ -->
<script src="{{static_url("components/bootstrap-tour/build/js/bootstrap-tour.min.js") }}" type="text/javascript" charset="utf-8"></script> <!-- window.Tour -->
<script src="{{static_url("components/jquery-typeahead/dist/jquery.typeahead.js") }}" type="text/javascript" charset="utf-8"></script> <!-- extends window.$ -->
<script src="{{static_url("components/jquery-typeahead/dist/jquery.typeahead.min.js") }}" type="text/javascript" charset="utf-8"></script> <!-- extends window.$ -->
<script src="{{static_url("components/codemirror/lib/codemirror.js") }}" type="text/javascript" charset="utf-8"></script>
<script src="{{static_url("components/codemirror/mode/meta.js") }}" type="text/javascript" charset="utf-8"></script>
<script src="{{static_url("components/requirejs/require.js") }}" type="text/javascript" charset="utf-8"></script>
<script>
window['staticURL'] = "{{static_url("", include_version=False)}}";
@ -38,7 +40,6 @@
backbone : 'components/backbone/backbone-min',
moment: 'components/moment/moment',
codemirror: 'components/codemirror',
termjs: 'components/term.js/src/term',
// Define aliases for requirejs webpack imports
notebook: 'built/index',
@ -162,6 +163,35 @@
}
},
});
define("bootstrap", function () {
return window.$;
});
define("jquery", function () {
return window.$;
});
define("jqueryui", function () {
return window.$;
});
define("jquery-ui", function () {
return window.$;
});
define("codemirror/lib/codemirror", function () {
return window.CodeMirror;
});
define("codemirror/mode/meta", function () {
return window.CodeMirror;
});
define("CodeMirror", function () {
return window.CodeMirror;
});
</script>
{% block meta %}

@ -12,10 +12,10 @@ from notebook.utils import url_path_join as ujoin
from .handlers import TerminalHandler, TermSocket
from . import api_handlers
def initialize(webapp, notebook_dir, connection_url):
shell = os.environ.get('SHELL') or 'sh'
def initialize(webapp, notebook_dir, connection_url, settings):
shell = settings.get('shell_command', [os.environ.get('SHELL') or 'sh'])
terminal_manager = webapp.settings['terminal_manager'] = NamedTermManager(
shell_command=[shell],
shell_command=shell,
extra_env={'JUPYTER_SERVER_ROOT': notebook_dir,
'JUPYTER_SERVER_URL': connection_url,
},

@ -71,21 +71,25 @@ casper.notebook_test(function () {
that.msgs = [];
that.on('remote.message', function(msg) {
that.msgs.push(msg);
})
});
that.evaluate(function (obj) {
for(var k in obj){
IPython.keyboard_manager.command_shortcuts.add_shortcut(k, function(){console.log(obj[k])});
if ({}.hasOwnProperty.call(obj, k)) {
IPython.keyboard_manager.command_shortcuts.add_shortcut(k, function(){console.log(obj[k]);});
}
}
}, shortcuts_test);
var longer_first = false;
var longer_last = false;
for(var m in that.msgs){
longer_first = longer_first||(that.msgs[m].match(/you are overriting/)!= null);
longer_last = longer_last ||(that.msgs[m].match(/will be shadowed/) != null);
if ({}.hasOwnProperty.call(that.msgs, m)) {
longer_first = longer_first||(that.msgs[m].match(/you are overriting/)!= null);
longer_last = longer_last ||(that.msgs[m].match(/will be shadowed/) != null);
}
}
this.test.assert(longer_first, 'no warning if registering shorter shortut');
this.test.assert(longer_last , 'no warning if registering longer shortut');
})
});
});

@ -1,14 +1,15 @@
casper.notebook_test(function () {
// Note, \033 is the octal notation of \u001b
// Test fixConsole
// Note, \u001b is the unicode notation of octal \033 which is not officially in js
var input = [
"\033[0m[\033[0minfo\033[0m] \033[0mtext\033[0m",
"\033[0m[\033[33mwarn\033[0m] \033[0m\tmore text\033[0m",
"\033[0m[\033[33mwarn\033[0m] \033[0m https://some/url/to/a/file.ext\033[0m",
"\033[0m[\033[31merror\033[0m] \033[0m\033[0m",
"\033[0m[\033[31merror\033[0m] \033[0m\teven more text\033[0m",
"\u001b[0m[\u001b[0minfo\u001b[0m] \u001b[0mtext\u001b[0m",
"\u001b[0m[\u001b[33mwarn\u001b[0m] \u001b[0m\tmore text\u001b[0m",
"\u001b[0m[\u001b[33mwarn\u001b[0m] \u001b[0m https://some/url/to/a/file.ext\u001b[0m",
"\u001b[0m[\u001b[31merror\u001b[0m] \u001b[0m\u001b[0m",
"\u001b[0m[\u001b[31merror\u001b[0m] \u001b[0m\teven more text\u001b[0m",
"\u001b[?25hBuilding wheels for collected packages: scipy",
"\x1b[38;5;28;01mtry\x1b[39;00m",
"\033[0m[\033[31merror\033[0m] \033[0m\t\tand more more text\033[0m",
"\u001b[0m[\u001b[31merror\u001b[0m] \u001b[0m\t\tand more more text\u001b[0m",
"normal\x1b[43myellowbg\x1b[35mmagentafg\x1b[1mbold\x1b[49mdefaultbg\x1b[39mdefaultfg\x1b[22mnormal",
].join("\n");
@ -29,7 +30,25 @@ casper.notebook_test(function () {
}, input);
this.test.assertEquals(result, output, "IPython.utils.fixConsole() handles [0m correctly");
// Test fixOverwrittenChars
var overwriting_test_cases = [
{input: "ABC\rDEF", result: "DEF"},
{input: "ABC\r\nDEF", result: "ABC\nDEF"},
{input: "123\b456", result: "12456"},
{input: "123\n\b456", result: "123\n\b456"},
{input: "\b456", result: "\b456"}
];
var that = this;
overwriting_test_cases.forEach(function(testcase){
var result = that.evaluate(function (input) {
return IPython.utils.fixOverwrittenChars(input);
}, testcase.input);
that.test.assertEquals(result, testcase.result, "Overwriting characters processed");
});
// Test load_extensions
this.thenEvaluate(function() {
define('nbextensions/a', [], function() { window.a = true; });
define('nbextensions/c', [], function() { window.c = true; });

@ -34,7 +34,7 @@ class TimeoutError(Exception):
class NotebookTestBase(TestCase):
"""A base class for tests that need a running notebook.
This create some empty config and runtime directories
and then starts the notebook server with them.
"""
@ -60,7 +60,7 @@ class NotebookTestBase(TestCase):
return
raise TimeoutError("The notebook server didn't start up correctly.")
@classmethod
def wait_until_dead(cls):
"""Wait for the server process to terminate after shutdown"""
@ -85,7 +85,7 @@ class NotebookTestBase(TestCase):
cls.data_dir = data_dir
cls.runtime_dir = TemporaryDirectory()
cls.notebook_dir = TemporaryDirectory()
started = Event()
def start_thread():
app = cls.notebook = NotebookApp(
@ -98,6 +98,7 @@ class NotebookTestBase(TestCase):
notebook_dir=cls.notebook_dir.name,
base_url=cls.url_prefix,
config=cls.config,
allow_root=True,
)
# don't register signal handler during tests
app.init_signal = lambda : None

@ -60,4 +60,46 @@ casper.notebook_test(function () {
var hashes = new Array(level + 1).join('#');
this.test.assertEquals(level_text, hashes + ' ' + text, 'markdown set_heading_level ' + level);
}
// Test markdown code blocks
function md_render_test (codeblock, result, message) {
// make a cell and trigger render
casper.thenEvaluate(function (text) {
var cell = Jupyter.notebook.insert_cell_at_bottom('markdown');
cell.set_text(text);
// signal window._rendered when cell render completes
window._rendered = null;
cell.events.one("rendered.MarkdownCell", function (event, data) {
window._rendered = data.cell.get_rendered();
});
cell.render();
}, {text: codeblock});
// wait for render to complete
casper.waitFor(function () {
return casper.evaluate(function () {
return window._rendered;
});
});
// test after waiting
casper.then(function () {
// get rendered result
var output = casper.evaluate(function () {
var rendered = window._rendered;
delete window._rendered;
return rendered;
});
// perform test
this.test.assertEquals(output.trim(), result, message);
});
};
var codeblock = '```\nx = 1\n```'
var result = '<pre><code>x = 1\n</code></pre>'
md_render_test(codeblock, result, 'Markdown code block no language');
codeblock = '```aaaa\nx = 1\n```'
result = '<pre><code class="cm-s-ipython language-aaaa">x = 1\n</code></pre>'
md_render_test(codeblock, result, 'Markdown code block unknown language');
});

@ -62,7 +62,15 @@ class TestInstallNBExtension(TestCase):
return py3compat.cast_unicode(td.name)
def setUp(self):
# Any TemporaryDirectory objects appended to this list will be cleaned
# up at the end of the test run.
self.tempdirs = []
@self.addCleanup
def cleanup_tempdirs():
for d in self.tempdirs:
d.cleanup()
self.src = self.tempdir()
self.files = files = [
pjoin(u'ƒile'),
@ -75,20 +83,30 @@ class TestInstallNBExtension(TestCase):
if not os.path.exists(parent):
os.makedirs(parent)
touch(fullpath)
self.data_dir = self.tempdir()
self.patch_data = patch.object(nbextensions,
'jupyter_data_dir', lambda : self.data_dir)
self.patch_data.start()
self.system_path = [self.tempdir()]
self.system_nbext = pjoin(self.system_path[0], 'nbextensions')
self.patch_system_data = patch.object(nbextensions,
'SYSTEM_JUPYTER_PATH', self.system_path)
self.patch_system_data.start()
def tearDown(self):
self.patch_data.stop()
self.patch_system_data.stop()
self.test_dir = self.tempdir()
self.data_dir = os.path.join(self.test_dir, 'data')
self.config_dir = os.path.join(self.test_dir, 'config')
self.system_data_dir = os.path.join(self.test_dir, 'system_data')
self.system_path = [self.system_data_dir]
self.system_nbext = os.path.join(self.system_data_dir, 'nbextensions')
# Patch out os.environ so that tests are isolated from the real OS
# environment.
self.patch_env = patch.dict('os.environ', {
'JUPYTER_CONFIG_DIR': self.config_dir,
'JUPYTER_DATA_DIR': self.data_dir,
})
self.patch_env.start()
self.addCleanup(self.patch_env.stop)
# Patch out the system path os that we consistently use our own
# temporary directory instead.
self.patch_system_path = patch.object(
nbextensions, 'SYSTEM_JUPYTER_PATH', self.system_path
)
self.patch_system_path.start()
self.addCleanup(self.patch_system_path.stop)
def assert_dir_exists(self, path):
if not os.path.exists(path):
@ -120,18 +138,20 @@ class TestInstallNBExtension(TestCase):
def test_create_data_dir(self):
"""install_nbextension when data_dir doesn't exist"""
with TemporaryDirectory() as td:
self.data_dir = pjoin(td, u'jupyter_data')
install_nbextension(self.src, user=True)
self.assert_dir_exists(self.data_dir)
for file in self.files:
self.assert_installed(
pjoin(basename(self.src), file),
user=True,
)
data_dir = os.path.join(td, self.data_dir)
with patch.dict('os.environ', {
'JUPYTER_DATA_DIR': data_dir,
}):
install_nbextension(self.src, user=True)
self.assert_dir_exists(data_dir)
for file in self.files:
self.assert_installed(
pjoin(basename(self.src), file),
user=True,
)
def test_create_nbextensions_user(self):
with TemporaryDirectory() as td:
self.data_dir = pjoin(td, u'jupyter_data')
install_nbextension(self.src, user=True)
self.assert_installed(
pjoin(basename(self.src), u'ƒile'),

@ -77,7 +77,7 @@ def test_nb_dir_root():
def test_generate_config():
with TemporaryDirectory() as td:
app = NotebookApp(config_dir=td)
app.initialize(['--generate-config'])
app.initialize(['--generate-config', '--allow-root'])
with nt.assert_raises(NoStart):
app.start()
assert os.path.exists(os.path.join(td, 'jupyter_notebook_config.py'))

@ -1,9 +1,18 @@
import os
from unittest import TestCase
try:
from unittest.mock import patch
except ImportError:
from mock import patch # py2
from ipython_genutils.tempdir import TemporaryDirectory
from ipython_genutils import py3compat
from traitlets.config.manager import BaseJSONConfigManager
from traitlets.tests.utils import check_help_all_output
from notebook.serverextensions import toggle_serverextension_python
from notebook import nbextensions
from notebook.nbextensions import _get_config_dir
@ -16,6 +25,34 @@ def test_help_output():
class TestInstallServerExtension(TestCase):
def tempdir(self):
td = TemporaryDirectory()
self.tempdirs.append(td)
return py3compat.cast_unicode(td.name)
def setUp(self):
self.tempdirs = []
self.test_dir = self.tempdir()
self.data_dir = os.path.join(self.test_dir, 'data')
self.config_dir = os.path.join(self.test_dir, 'config')
self.system_data_dir = os.path.join(self.test_dir, 'system_data')
self.system_path = [self.system_data_dir]
self.patch_env = patch.dict('os.environ', {
'JUPYTER_CONFIG_DIR': self.config_dir,
'JUPYTER_DATA_DIR': self.data_dir,
})
self.patch_env.start()
self.patch_system_path = patch.object(nbextensions,
'SYSTEM_JUPYTER_PATH', self.system_path)
self.patch_system_path.start()
def tearDown(self):
self.patch_env.stop()
self.patch_system_path.stop()
def _inject_mock_extension(self):
outer_file = __file__

@ -132,7 +132,7 @@ def is_hidden(abs_path, abs_root=''):
if sys.platform == 'win32':
try:
attrs = ctypes.windll.kernel32.GetFileAttributesW(py3compat.cast_unicode(path))
attrs = ctypes.windll.kernel32.GetFileAttributesW(py3compat.cast_unicode(abs_path))
except AttributeError:
pass
else:

@ -9,23 +9,37 @@
"url": "https://github.com/jupyter/notebook.git"
},
"scripts": {
"lint": "eslint --quiet notebook/",
"bower": "bower install --allow-root --config.interactive=false",
"build:watch": "concurrent \"npm run build:css:watch\" \"npm run build:js:watch\"",
"build": "npm run build:css && npm run build:js",
"build:css": "python setup.py css",
"build:css:watch": "echo Not implemented yet...",
"build": "npm run build:js && npm run build:css",
"build:css": "concurrent \"npm run build:css:ipython\" \"npm run build:css:style\"",
"build:css:ipython": "lessc --include-path=notebook/static notebook/static/style/ipython.less notebook/static/style/ipython.min.css",
"build:css:style": "lessc --include-path=notebook/static notebook/static/style/style.less notebook/static/style/style.min.css",
"build:css:watch": "./scripts/less-watch ./notebook/static",
"build:js": "webpack",
"build:js:watch": "npm run build:js -- --watch"
},
"devDependencies": {
"babel-cli": "^6.7.5",
"babel-core": "^6.7.4",
"babel-loader": "^6.2.4",
"babel-preset-es2015": "^6.6.0",
"bower": "*",
"concurrently": "^1.0.0",
"css-loader": "^0.23.1",
"eslint": "^2.8.0",
"json-loader": "^0.5.4",
"less": "~2",
"requirejs": "^2.1.17",
"style-loader": "^0.13.1",
"underscore": "^1.8.3",
"webpack": "^1.12.13"
},
"dependencies": {
"moment": "^2.8.4"
"moment": "^2.8.4",
"preact": "^4.5.1",
"preact-compat": "^1.7.0",
"xterm": "^1.0"
}
}

@ -0,0 +1,39 @@
#!/usr/bin/env node
/**
Usage:
./scripts/less-watch [watchPath]
Example:
./scripts/less-watch ./notebook/static/notebook/less
**/
var less = require('less');
var fs = require('fs');
var path = require('path');
var child_process = require('child_process');
function watchDir(dir) {
var rootPath = path.join(__dirname, '..');
var watchPath = path.resolve(dir);
console.log('less-watch:', 'watching:', path.relative(rootPath, watchPath));
fs.watch(watchPath, {recursive: true}, function(event, file) {
if (file && /.+\.less$/.test(file)) {
console.log('less-watch:', 'modified:', file);
child_process.exec('lessc --include-path=notebook/static --verbose notebook/static/style/style.less notebook/static/style/style.min.css', function(err, stdout, stderr) {
if (err) return console.log(err);
if (stdout) console.log(stdout);
if (stderr) console.log(stderr);
});
child_process.exec('lessc --include-path=notebook/static notebook/static/style/ipython.less notebook/static/style/ipython.min.css', function(err, stdout, stderr) {
if (err) return console.log(err);
if (stdout) console.log(stdout);
if (stderr) console.log(stderr);
});
}
});
}
watchDir(process.argv[2]);

@ -78,7 +78,7 @@ for more information.
version = version,
scripts = glob(pjoin('scripts', '*')),
packages = find_packages(),
package_data = find_package_data(),
package_data = find_package_data(),
author = 'Jupyter Development Team',
author_email = 'jupyter@googlegroups.com',
url = 'http://jupyter.org',
@ -146,7 +146,7 @@ install_requires = [
'jinja2',
'tornado>=4',
'ipython_genutils',
'traitlets',
'traitlets>=4.2.1',
'jupyter_core',
'jupyter_client',
'nbformat',
@ -163,8 +163,6 @@ if 'setuptools' in sys.modules:
# setup.py develop should check for submodules
from setuptools.command.develop import develop
setup_args['cmdclass']['develop'] = css_js_prerelease(develop)
if not PY3:
setup_args['setup_requires'] = ['ipython_genutils']
try:
from wheel.bdist_wheel import bdist_wheel

@ -19,9 +19,9 @@ import sys
import pipes
from distutils import log
from distutils.cmd import Command
from distutils.version import LooseVersion
from fnmatch import fnmatch
from glob import glob
from multiprocessing.pool import ThreadPool
from subprocess import check_call, check_output
if sys.platform == 'win32':
@ -129,9 +129,6 @@ def find_package_data():
pjoin('static', 'services', 'built', '*contents.js'),
pjoin('static', 'services', 'built', '*contents.js.map'),
])
# Add the Lab page contents
static_data.append(pjoin('lab', 'build', '*'))
components = pjoin("static", "components")
# select the components we actually need to install
@ -156,7 +153,6 @@ def find_package_data():
pjoin(components, "underscore", "underscore-min.js"),
pjoin(components, "moment", "moment.js"),
pjoin(components, "moment", "min", "moment.min.js"),
pjoin(components, "term.js", "src", "term.js"),
pjoin(components, "text-encoding", "lib", "encoding.js"),
])
@ -331,6 +327,27 @@ def run(cmd, *args, **kwargs):
return check_call(cmd, *args, **kwargs)
def npm_install(cwd):
"""Run npm install in a directory and dedupe if necessary"""
try:
run(['npm', 'install', '--progress=false'], cwd=cwd)
except OSError as e:
print("Failed to run `npm install`: %s" % e, file=sys.stderr)
print("npm is required to build a development version of the notebook.", file=sys.stderr)
raise
shell = (sys.platform == 'win32')
version = check_output(['npm', '--version'], shell=shell).decode('utf-8')
if LooseVersion(version) < LooseVersion('3.0'):
try:
run(['npm', 'dedupe'], cwd=cwd)
except Exception as e:
print("Failed to run `npm dedupe`: %s" % e, file=sys.stderr)
print("Please install npm v3+ to build a development version of the notebook.")
raise
class JavascriptDependencies(Command):
description = "Fetch Javascript dependencies with npm and bower"
@ -342,16 +359,10 @@ class JavascriptDependencies(Command):
bower_dir = pjoin(static, 'components')
node_modules = pjoin(repo_root, 'node_modules')
lab_dir = pjoin(repo_root, 'notebook', 'lab')
def run(self):
try:
run(['npm', 'install', '--progress=false'], cwd=repo_root)
except OSError as e:
print("Failed to run `npm install`: %s" % e, file=sys.stderr)
print("npm is required to build a development version of the notebook.", file=sys.stderr)
raise
npm_install(repo_root)
try:
run(['npm', 'run', 'bower'], cwd=repo_root)
except Exception as e:
@ -359,14 +370,6 @@ class JavascriptDependencies(Command):
print("You can install js dependencies with `npm install`", file=sys.stderr)
raise
try:
run(['npm', 'install', '--progress=false'], cwd=self.lab_dir)
run(['npm', 'run', 'build'], cwd=self.lab_dir)
except Exception as e:
print("Failed to run `npm install`: %s" % e, file=sys.stderr)
print("You can install js dependencies with `npm install`", file=sys.stderr)
raise
# update package data in case this created new files
update_package_data(self.distribution)
@ -387,29 +390,17 @@ class CompileCSS(Command):
def finalize_options(self):
pass
sources = []
targets = []
for name in ('ipython', 'style'):
sources.append(pjoin(static, 'style', '%s.less' % name))
targets.append(pjoin(static, 'style', '%s.min.css' % name))
def run(self):
self.run_command('jsdeps')
env = os.environ.copy()
env['PATH'] = npm_path
for src, dst in zip(self.sources, self.targets):
try:
run(['lessc',
'--source-map',
'--include-path=%s' % pipes.quote(static),
src,
dst,
], cwd=repo_root, env=env)
except OSError as e:
print("Failed to build css: %s" % e, file=sys.stderr)
print("You can install js dependencies with `npm install`", file=sys.stderr)
raise
try:
run(['npm', 'run', 'build:css'])
except OSError as e:
print("Failed to run npm run build:css : %s" % e, file=sys.stderr)
print("You can install js dependencies with `npm install`", file=sys.stderr)
raise
# update package data in case this created new files
update_package_data(self.distribution)
@ -526,8 +517,8 @@ def css_js_prerelease(command, strict=False):
return
try:
self.distribution.run_command('css')
self.distribution.run_command('js')
self.distribution.run_command('css')
except Exception as e:
# refresh missing
missing = [ t for t in targets if not os.path.exists(t) ]

@ -1,6 +1,12 @@
var _ = require('underscore');
var path = require('path');
var sourcemaps = 'source-map'
if(process.argv.indexOf('-w') !== -1 || process.argv.indexOf('-w') !== -1 ){
console.log('watch mode detected, will switch to cheep sourcemaps')
sourcemaps = 'eval-source-map';
}
var commonConfig = {
resolve: {
root: [
@ -12,8 +18,10 @@ var commonConfig = {
"node_modules" /* npm */
]
},
bail: true,
module: {
loaders: [
{ test: /\.js$/, exclude: /node_modules|\/notebook\/static\/component/, loader: "babel-loader"},
{ test: /\.css$/, loader: "style-loader!css-loader" },
{ test: /\.json$/, loader: "json-loader" },
// jquery-ui loads some images
@ -31,7 +39,13 @@ var commonConfig = {
bootstrap: '$',
bootstraptour: 'Tour',
'jquery-ui': '$',
typeahead: '$.typeahead'
typeahead: '$.typeahead',
'codemirror': 'CodeMirror',
'codemirror/lib/codemirror': 'CodeMirror',
'codemirror/mode/meta': 'CodeMirror',
// Account for relative paths from other CodeMirror files
'../../lib/codemirror': 'CodeMirror',
'../lib/codemirror': 'CodeMirror'
}
};
@ -43,7 +57,7 @@ function buildConfig(appName) {
filename: 'main.min.js',
path: './notebook/static/' + appName + '/js/built'
},
devtool: 'source-map',
devtool: sourcemaps,
});
}
@ -60,7 +74,7 @@ module.exports = [
path: './notebook/static/services/built',
libraryTarget: 'amd'
},
devtool: 'source-map',
devtool: sourcemaps,
}),
_.extend({}, commonConfig, {
entry: './notebook/static/index.js',
@ -69,6 +83,6 @@ module.exports = [
path: './notebook/static/built',
libraryTarget: 'amd'
},
devtool: 'source-map',
devtool: sourcemaps,
}),
].map(buildConfig);

Loading…
Cancel
Save