Browse Source

Merge pull request #148 from blink1073/cell-metadata

Implement user metadata for cells and clean up notebook metadata
Dave Willmer 9 years ago
parent
commit
ddd206ac5f

+ 259 - 81
src/notebook/cells/model.ts

@@ -2,12 +2,16 @@
 // Distributed under the terms of the Modified BSD License.
 'use strict';
 
+import {
+  shallowEquals
+} from 'jupyter-js-utils';
+
 import {
   IDisposable
 } from 'phosphor-disposable';
 
 import {
-  IChangedArgs, Property
+  IChangedArgs
 } from 'phosphor-properties';
 
 import {
@@ -59,6 +63,11 @@ interface IBaseCellModel extends IDisposable {
    */
   stateChanged: ISignal<IBaseCellModel, IChangedArgs<any>>;
 
+  /**
+   * A signal emitted when a user metadata state changes.
+   */
+  metadataChanged: ISignal<IBaseCellModel, string>;
+
   /**
    * The input area of the cell.
    *
@@ -78,6 +87,24 @@ interface IBaseCellModel extends IDisposable {
    * Whether the cell is read only.
    */
   readOnly: boolean;
+
+  /**
+   * Get a metadata cursor for the cell.
+   *
+   * #### Notes
+   * Metadata associated with the nbformat spec are set directly
+   * on the model.  This method is used to interact with a namespaced
+   * set of metadata on the cell.
+   */
+  getMetadata(name: string): IMetadataCursor;
+
+  /**
+   * List the metadata namespace keys for the notebook.
+   *
+   * #### Notes
+   * Metadata associated with the nbformat are not included.
+   */
+  listMetadata(): string[];
 }
 
 
@@ -161,6 +188,16 @@ class BaseCellModel implements IBaseCellModel {
     return CellModelPrivate.stateChangedSignal.bind(this);
   }
 
+  /**
+   * A signal emitted when a user metadata state changes.
+   *
+   * #### Notes
+   * The signal argument is the namespace of the metadata that changed.
+   */
+  get metadataChanged(): ISignal<IBaseCellModel, string> {
+    return CellModelPrivate.metadataChangedSignal.bind(this);
+  }
+
   /**
    * Get the input area model.
    */
@@ -187,30 +224,58 @@ class BaseCellModel implements IBaseCellModel {
    * See http://jupyter-notebook.readthedocs.org/en/latest/security.html.
    */
   get trusted(): boolean {
-    return CellModelPrivate.trustedProperty.get(this);
+    return this._trusted;
   }
   set trusted(value: boolean) {
-    CellModelPrivate.trustedProperty.set(this, value);
+    if (value === this._trusted) {
+      return;
+    }
+    let prev = this._trusted;
+    this._trusted = value;
+    this.onTrustChanged(value);
+    this.stateChanged.emit({
+      name: 'trusted',
+      oldValue: prev,
+      newValue: value
+    });
   }
 
   /**
    * The name of the cell.
    */
   get name(): string {
-    return CellModelPrivate.nameProperty.get(this);
+    return this._name;
   }
   set name(value: string) {
-    CellModelPrivate.nameProperty.set(this, value);
+    if (value === this._name) {
+      return;
+    }
+    let prev = this._name;
+    this._name = value;
+    this.stateChanged.emit({
+      name: 'name',
+      oldValue: prev,
+      newValue: value
+    });
   }
 
   /**
    * The tags for the cell.
    */
   get tags(): string[] {
-    return CellModelPrivate.tagsProperty.get(this);
+    return JSON.parse(this._tags);
   }
   set tags(value: string[]) {
-    CellModelPrivate.tagsProperty.set(this, value);
+    let prev = JSON.parse(this._tags);
+    if (shallowEquals(prev, value)) {
+      return;
+    }
+    this._tags = JSON.stringify(value);
+    this.stateChanged.emit({
+      name: 'tags',
+      oldValue: prev,
+      newValue: value
+    });
   }
 
   /**
@@ -236,12 +301,66 @@ class BaseCellModel implements IBaseCellModel {
     this._input = null;
   }
 
+  /**
+   * Get a metadata cursor for the cell.
+   *
+   * #### Notes
+   * Metadata associated with the nbformat spec are set directly
+   * on the model.  This method is used to interact with a namespaced
+   * set of metadata on the cell.
+   */
+  getMetadata(name: string): IMetadataCursor {
+    let blacklist = ['tags', 'name', 'trusted', 'collapsed', 'scrolled',
+                     'execution_count', 'format'];
+    if (blacklist.indexOf(name) !== -1) {
+      let key = blacklist[blacklist.indexOf(name)];
+      throw Error(`Use model attribute for ${key} directly`);
+    }
+    return new MetadataCursor(
+      name,
+      this._metadata[name],
+      this._cursorCallback
+    );
+  }
+
+  /**
+   * List the metadata namespace keys for the notebook.
+   *
+   * #### Notes
+   * Metadata associated with the nbformat are not included.
+   */
+  listMetadata(): string[] {
+    return Object.keys(this._metadata);
+  }
+
+  /**
+   * Handle changes to cell trust.
+   *
+   * #### Notes
+   * The default implementation is a no-op.
+   */
+  protected onTrustChanged(value: boolean): void {
+
+  }
+
+  /**
+   * The singleton callback for cursor change events.
+   */
+  private _cursorCallback(name: string, value: string): void {
+    this._metadata[name] = value;
+    this.metadataChanged.emit(name);
+  }
+
   /**
    * The type of cell.
    */
   type: CellType;
 
   private _input: IInputAreaModel = null;
+  private _tags = '';
+  private _name = '';
+  private _trusted = false;
+  private _metadata: { [key: string]: string } = Object.create(null);
 }
 
 
@@ -267,12 +386,11 @@ class CodeCellModel extends BaseCellModel implements ICodeCellModel {
   }
 
   /**
-   * Set the trusted state of the model.
+   * Handle changes to cell trust.
    *
    * See http://jupyter-notebook.readthedocs.org/en/latest/security.html.
    */
-  set trusted(value: boolean) {
-    CellModelPrivate.trustedProperty.set(this, value);
+  protected onTrustChanged(value: boolean): void {
     this.output.trusted = value;
   }
 
@@ -280,31 +398,58 @@ class CodeCellModel extends BaseCellModel implements ICodeCellModel {
    * The execution count.
    */
   get executionCount(): number {
-    return CellModelPrivate.executionCountProperty.get(this);
+    return this._executionCount;
   }
   set executionCount(value: number) {
-    CellModelPrivate.executionCountProperty.set(this, value);
+    if (this._executionCount === value) {
+      return;
+    }
+    let prev = this._executionCount;
+    this._executionCount = value;
     this.input.prompt = `In [${value === null ? ' ' : value}]:`;
+    this.stateChanged.emit({
+      name: 'executionCount',
+      oldValue: prev,
+      newValue: value
+    });
   }
 
   /**
    * Whether the cell is collapsed/expanded.
    */
   get collapsed(): boolean {
-    return CellModelPrivate.collapsedProperty.get(this);
+    return this._collapsed;
   }
   set collapsed(value: boolean) {
-    CellModelPrivate.collapsedProperty.set(this, value);
+    if (this._collapsed === value) {
+      return;
+    }
+    let prev = this._collapsed;
+    this._collapsed = value;
+    this.stateChanged.emit({
+      name: 'collapsed',
+      oldValue: prev,
+      newValue: value
+    });
   }
 
   /**
    * Whether the cell's output is scrolled, unscrolled, or autoscrolled.
    */
   get scrolled(): ScrollSetting {
-    return CellModelPrivate.scrolledProperty.get(this);
+    return this._scrolled;
   }
   set scrolled(value: ScrollSetting) {
-    CellModelPrivate.scrolledProperty.set(this, value);
+    if (this._scrolled === value) {
+      return;
+    }
+    let prev = this._scrolled;
+    this._scrolled = value;
+    this.stateChanged.emit({
+      name: 'scrolled',
+      oldValue: prev,
+      newValue: value
+    });
   }
 
   /**
@@ -318,6 +463,9 @@ class CodeCellModel extends BaseCellModel implements ICodeCellModel {
   type: CellType = 'code';
 
   private _output: IOutputAreaModel = null;
+  private _scrolled: ScrollSetting = false;
+  private _collapsed = false;
+  private _executionCount = -1;
 }
 
 
@@ -330,13 +478,23 @@ class MarkdownCellModel extends BaseCellModel implements IMarkdownCellModel {
    * Whether we should display a rendered representation.
    */
   get rendered() {
-    return CellModelPrivate.renderedProperty.get(this);
+    return this._rendered;
   }
   set rendered(value: boolean) {
-    CellModelPrivate.renderedProperty.set(this, value);
+    if (this._rendered === value) {
+      return;
+    }
+    let prev = this._rendered;
+    this._rendered = value;
+    this.stateChanged.emit({
+      name: 'rendered',
+      oldValue: prev,
+      newValue: value
+    });
   }
 
   type: CellType = 'markdown';
+  private _rendered = true;
 }
 
 
@@ -349,13 +507,23 @@ class RawCellModel extends BaseCellModel implements IRawCellModel {
    * The raw cell metadata format for nbconvert.
    */
   get format(): string {
-    return CellModelPrivate.formatProperty.get(this);
+    return this._format;
   }
   set format(value: string) {
-    CellModelPrivate.formatProperty.set(this, value);
+    if (this._format === value) {
+      return;
+    }
+    let prev = this._format;
+    this._format = value;
+    this.stateChanged.emit({
+      name: 'format',
+      oldValue: prev,
+      newValue: value
+    });
   }
 
   type: CellType = 'raw';
+  private _format = '';
 }
 
 
@@ -395,78 +563,88 @@ namespace CellModelPrivate {
   const stateChangedSignal = new Signal<IBaseCellModel, IChangedArgs<any>>();
 
   /**
-   * A property descriptor for the name of a cell.
+   * A signal emitted when a user metadata state changes.
    */
   export
-  const nameProperty = new Property<IBaseCellModel, string>({
-    name: 'name',
-    value: null,
-    notify: stateChangedSignal
-  });
+  const metadataChangedSignal = new Signal<IBaseCellModel, string>();
+}
+
 
+
+/**
+ * A class used to interact with user level metadata.
+ */
+export
+interface IMetadataCursor {
   /**
-   * A property descriptor for the tags of a cell.
+   * The metadata namespace.
    */
-  export
-  const tagsProperty = new Property<IBaseCellModel, string[]>({
-    name: 'tags',
-    value: null,
-    notify: stateChangedSignal
-  });
-
- /**
-  * A property descriptor for the trusted state of a cell.
-  */
-  export
-  const trustedProperty = new Property<IBaseCellModel, boolean>({
-    name: 'trusted',
-    notify: stateChangedSignal
-  });
+  name: string;
 
- /**
-  * A property descriptor holding the format of a raw cell.
-  */
-  export
-  const formatProperty = new Property<IRawCellModel, string>({
-    name: 'format',
-    notify: stateChangedSignal
-  });
+  /**
+   * Get the value of the metadata.
+   */
+  getValue(): any;
 
   /**
-   * A property descriptor which determines whether the input area should be rendered.
+   * Set the value of the metdata.
    */
-  export
-  const renderedProperty = new Property<IMarkdownCellModel, boolean>({
-    name: 'rendered',
-    value: true,
-    notify: stateChangedSignal
-  });
+  setValue(value: any): void;
+}
+
+
+/**
+ * An implementation of a metadata cursor.
+ */
+export
+class MetadataCursor implements IMetadataCursor {
 
   /**
-   * A property descriptor for the execution count of a code cell.
+   * Construct a new metadata cursor.
+   *
+   * @param name - the metadata namespace key.
+   *
+   * @param value - this initial value of the namespace.
+   *
+   * @param cb - a change callback.
    */
-  export
-  const executionCountProperty = new Property<ICodeCellModel, number>({
-    name: 'executionCount',
-    value: null,
-    notify: stateChangedSignal
-  });
-
- /**
-  * A property descriptor for the collapsed state of a code cell.
-  */
-  export
-  const collapsedProperty = new Property<ICodeCellModel, boolean>({
-    name: 'collapsed',
-    notify: stateChangedSignal
-  });
+  constructor(name: string, value: string, cb: (name: string, value: string) => void) {
+    this._name = name;
+    this._value = value || 'null';
+    this._cb = cb;
+  }
 
- /**
-  * A property descriptor for the scrolled state of a code cell.
-  */
-  export
-  const scrolledProperty = new Property<ICodeCellModel, ScrollSetting>({
-    name: 'scrolled',
-    notify: stateChangedSignal
-  });
+  /**
+   * Get the namespace key of the metadata.
+   *
+   * #### Notes
+   * This is a read-only property.
+   */
+  get name(): string {
+    return this._name;
+  }
+
+  /**
+   * Get the value of the namespace data.
+   */
+  getValue(): any {
+    return JSON.parse(this._value);
+  }
+
+  /**
+   * Set the value of the namespace data.
+   */
+  setValue(value: string): any {
+    let prev = this._value;
+    if (prev === value) {
+      return;
+    }
+    this._value = JSON.stringify(value);
+    let cb = this._cb;
+    cb(this._name, this._value);
+  }
+
+  private _value = 'null';
+  private _name = '';
+  private _cb: (name: string, value: string) => void = null;
 }

+ 12 - 90
src/notebook/notebook/model.ts

@@ -46,7 +46,7 @@ import {
   ICodeCellModel, CodeCellModel,
   IMarkdownCellModel, MarkdownCellModel,
   IRawCellModel, isCodeCellModel, isMarkdownCellModel,
-  RawCellModel, isRawCellModel
+  RawCellModel, isRawCellModel, MetadataCursor, IMetadataCursor
 } from '../cells';
 
 import {
@@ -77,28 +77,6 @@ const DEFAULT_LANG_INFO = {
 }
 
 
-/**
- * A class used to interact with user level notebook metadata.
- */
-export
-interface INotebookMetadataCursor {
-  /**
-   * The metadata namespace.
-   */
-  name: string;
-
-  /**
-   * Get the value of the metadata.
-   */
-  getValue(): any;
-
-  /**
-   * Set the value of the metdata.
-   */
-  setValue(value: any): void;
-}
-
-
 /**
  * The definition of a model object for a notebook widget.
  */
@@ -227,7 +205,7 @@ interface INotebookModel extends IDisposable {
    * on the model.  This method is used to interact with a namespaced
    * set of metadata on the notebook.
    */
-  getMetadata(namespace: string): INotebookMetadataCursor;
+  getMetadata(namespace: string): IMetadataCursor;
 
   /**
    * List the metadata namespace keys for the notebook.
@@ -366,14 +344,14 @@ class NotebookModel implements INotebookModel {
    * The kernelspec metadata for the notebook.
    */
   get kernelspec(): IKernelspecMetadata {
-    return this._kernelspec;
+    return JSON.parse(this._kernelspec);
   }
   set kernelspec(value: IKernelspecMetadata) {
-    let prev = this._kernelspec;
+    let prev = JSON.parse(this._kernelspec);
     if (prev === value) {
       return;
     }
-    this._kernelspec = Object.freeze(value);
+    this._kernelspec = JSON.stringify(value);
     this.stateChanged.emit({
       name: 'kernelspec',
       oldValue: prev,
@@ -385,14 +363,14 @@ class NotebookModel implements INotebookModel {
    * The language info metadata for the notebook.
    */
   get languageInfo(): ILanguageInfoMetadata {
-    return this._langInfo;
+    return JSON.parse(this._langInfo);
   }
   set languageInfo(value: ILanguageInfoMetadata) {
-    let prev = this._langInfo;
+    let prev = JSON.parse(this._langInfo);
     if (shallowEquals(prev, value)) {
       return;
     }
-    this._langInfo = Object.freeze(value);
+    this._langInfo = JSON.stringify(value);
     this.stateChanged.emit({
       name: 'languageInfo',
       oldValue: prev,
@@ -644,13 +622,13 @@ class NotebookModel implements INotebookModel {
    * on the model.  This method is used to interact with a namespaced
    * set of metadata on the notebook.
    */
-  getMetadata(name: string): INotebookMetadataCursor {
+  getMetadata(name: string): IMetadataCursor {
     let invalid = ['kernelspec', 'languageInfo', 'origNbformat'];
     if (invalid.indexOf(name) !== -1) {
       let key = invalid[invalid.indexOf(name)];
       throw Error(`Use model attribute for ${key} directly`);
     }
-    return new NotebookMetadataCursor(
+    return new MetadataCursor(
       name,
       this._metadata[name],
       this._cursorCallback
@@ -753,8 +731,8 @@ class NotebookModel implements INotebookModel {
   private _defaultMimetype = 'text/x-ipython';
   private _readOnly = false;
   private _session: INotebookSession = null;
-  private _kernelspec: IKernelspecMetadata = DEFAULT_KERNELSPEC;
-  private _langInfo: ILanguageInfoMetadata = DEFAULT_LANG_INFO;
+  private _kernelspec = JSON.stringify(DEFAULT_KERNELSPEC);
+  private _langInfo = JSON.stringify(DEFAULT_LANG_INFO);
   private _origNbformat: number = null;
   private _activeCellIndex = -1;
   private _mode: NotebookMode = 'command';
@@ -867,59 +845,3 @@ namespace NotebookModelPrivate {
   }
 
 }
-
-
-/**
- * An implementation of a notebook metadata cursor.
- */
-class NotebookMetadataCursor implements INotebookMetadataCursor {
-
-  /**
-   * Construct a new notebook metadata cursor.
-   *
-   * @param name - the metadata namespace key.
-   *
-   * @param value - this initial value of the namespace.
-   *
-   * @param cb - a change callback.
-   */
-  constructor(name: string, value: string, cb: (name: string, value: string) => void) {
-    this._name = name;
-    this._value = value || 'null';
-    this._cb = cb;
-  }
-
-  /**
-   * Get the namespace key of the metadata.
-   *
-   * #### Notes
-   * This is a read-only property.
-   */
-  get name(): string {
-    return this._name;
-  }
-
-  /**
-   * Get the value of the namespace data.
-   */
-  getValue(): any {
-    return JSON.parse(this._value);
-  }
-
-  /**
-   * Set the value of the namespace data.
-   */
-  setValue(value: string): any {
-    let prev = this._value;
-    if (prev === value) {
-      return;
-    }
-    this._value = JSON.stringify(value);
-    let cb = this._cb;
-    cb(this._name, this._value);
-  }
-
-  private _value = 'null';
-  private _name = '';
-  private _cb: (name: string, value: string) => void = null;
-}

+ 12 - 1
src/notebook/notebook/nbformat.ts

@@ -38,12 +38,23 @@ interface ILanguageInfoMetadata {
 }
 
 
+/**
+ * The default metadata for the notebook.
+ */
+export
+interface INotebookMetadata {
+  kernelspec: IKernelspecMetadata;
+  language_info: ILanguageInfoMetadata;
+  orig_nbformat: number;
+}
+
+
 /**
  * The notebook content.
  */
 export
 interface INotebookContent {
-  metadata: any;
+  metadata: INotebookMetadata;
   nbformat_minor: number;
   nbformat: number;
   cells: ICell[];

+ 19 - 6
src/notebook/notebook/serialize.ts

@@ -10,7 +10,7 @@ import {
 import {
   INotebookContent, ICell, MAJOR_VERSION, MINOR_VERSION,
   IRawCell, ICodeCell, isRawCell, isMarkdownCell,
-  isCodeCell
+  isCodeCell, INotebookMetadata
 } from './nbformat';
 
 import {
@@ -28,14 +28,14 @@ function serialize(nb: INotebookModel): INotebookContent {
     let cell = nb.cells.get(i);
     cells.push(serializeCell(cell));
   }
-  let metadata: any = {
+  let metadata: INotebookMetadata = {
     kernelspec: nb.kernelspec,
     language_info: nb.languageInfo,
     orig_nbformat: nb.origNbformat
   }
   for (let key of nb.listMetadata()) {
     let cursor = nb.getMetadata(key);
-    metadata[key] = cursor.getValue();
+    (metadata as any)[key] = cursor.getValue();
   }
   return {
     cells: cells,
@@ -76,7 +76,7 @@ function deserialize(data: INotebookContent, model: INotebookModel): void {
 
   if (data && data.metadata) {
     let metadata = data.metadata;
-    for (let key of data.metadata) {
+    for (let key of Object.keys(data.metadata)) {
       switch(key) {
       case 'kernelspec':
         model.kernelspec = metadata.kernelspec;
@@ -85,11 +85,11 @@ function deserialize(data: INotebookContent, model: INotebookModel): void {
         model.languageInfo = metadata.language_info;
         break;
       case 'orig_nbformat':
-        model.origNbformat = metadata.origNbformat;
+        model.origNbformat = metadata.orig_nbformat;
         break;
       default:
         let cursor = model.getMetadata(key);
-        cursor.setValue(metadata[key]);
+        cursor.setValue((metadata as any)[key]);
       }
     }
   }
@@ -124,6 +124,10 @@ function serializeCell(cell: ICellModel): ICell {
     }
     out.execution_count = cell.executionCount;
   }
+  for (let key of cell.listMetadata()) {
+    let cursor = cell.getMetadata(key);
+    (output.metadata as any)[key] = cursor.getValue();
+  }
   return output;
 }
 
@@ -150,4 +154,13 @@ function deserializeCell(data: ICell, model: ICellModel): void {
   } else if (isRawCellModel(model)) {
     (model as IRawCellModel).format = (data as IRawCell).metadata.format;
   }
+  let metadata = data.metadata;
+  let blacklist = ['tags', 'name', 'trusted', 'collapsed', 'scrolled',
+                   'execution_count', 'format'];
+  for (let key of Object.keys(metadata)) {
+    if (blacklist.indexOf(key) === -1) {
+      let cursor = model.getMetadata(key);
+      cursor.setValue((metadata as any)[key]);
+    }
+  }
 }