Skip to content

Commit 87778d4

Browse files
committed
Added cell-id proposal
1 parent 76f45b2 commit 87778d4

File tree

1 file changed

+314
-0
lines changed

1 file changed

+314
-0
lines changed

62-cell-id/cell-id.md

Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
---
2+
title: Cell ID Addition to Notebook Format
3+
authors: Matthew Seal ([@MSeal](https://github.com/MSeal)) and Carol Willing ([@willingc](https://github.com/willingc))
4+
issue-number: 61
5+
pr-number: 62
6+
date-started: "2020-08-27"
7+
type: S - [Standards Track](https://www.python.org/dev/peps/#pep-types-key)
8+
---
9+
10+
# Cell ID Addition to Notebook Format
11+
12+
## Problem
13+
14+
Modern applications need a mechanism for referencing and recalling particular cells within a notebook. Referencing and recalling cells are needed across notebooks' mutation inside a specific notebook session and in future notebook sessions.
15+
16+
Some application examples include:
17+
18+
- generating URL links to specific cells
19+
- associating an external document to the cell for applications like code reviews, annotations, or comments
20+
- comparing a cell's output across multiple runs
21+
22+
### Existing limitation
23+
24+
Traditionally custom `tags` on cells have been used to track particular use-cases for cell activity. Custom `tags` work well for some things like identifying the class of content within a cell (e.g., papermill `parameters` cell tag). The `tags` approach falls short when an application needs to associate a cell with an action or resource **dynamically**. Additionally, the lack of a cell id field has led to applications generating ids in different proprietary or non-standard ways (e.g. `metadata["cell_id"] = "some-string"` vs `metadata[application_name]["id"] = cell_guuid`).
25+
26+
### Scope of the JEP
27+
28+
Most resource applications include ids as a standard part of the resource / sub-resources. **This proposal focuses only on a cell ID**.
29+
30+
Out of scope for this proposal is an overall notebook id field. The sub-resource of cells is often treated relationally, so even without adding a notebook id; thin scope change would improve the quality of abstractions built on-top of notebooks. The intention is to focus on notebook id patterns after cell ids.
31+
32+
## The Motivation for a JEP
33+
34+
The responses to these two questions define requiring a JEP:
35+
36+
*1. Does the proposal/implementation PR impact multiple orgs, or have widespread community impact?*
37+
38+
- Yes, this JEP updates nbformat.
39+
40+
*2. Does the proposal/implementation change an invariant in one or more orgs?*
41+
42+
- Yes, the JEP proposes a unique cell identifier.
43+
44+
This proposal covers both questions.
45+
46+
## Proposed Enhancement
47+
48+
### Adding an `id` field
49+
50+
This change would add an `id` field to each cell type in the [4.4 json_schema](https://github.com/jupyter/nbformat/blob/master/nbformat/v4/nbformat.v4.4.schema.json). Specifically, the [raw_cell](https://github.com/jupyter/nbformat/blob/master/nbformat/v4/nbformat.v4.4.schema.json#L114), [markdown](https://github.com/jupyter/nbformat/blob/master/nbformat/v4/nbformat.v4.4.schema.json#L151), and [code_cell](https://github.com/jupyter/nbformat/blob/master/nbformat/v4/nbformat.v4.4.schema.json#L184) required sections would add the `id` field with the following schema:
51+
52+
```
53+
"id": {
54+
"description": "A str field representing the identifier of this particular cell.",
55+
"type": "string",
56+
"pattern": "^[a-zA-Z0-9-]+$",
57+
"minLength": 2,
58+
"maxLength": 36
59+
}
60+
```
61+
62+
This change is **not** an addition to the cells' `metadata` space, which has an `additionalProperties: true` attribute. This is adding to the cell definitions directly at the same level as `metadata`, in which scope `additionalProperties` is false and there's no potential for collision of existing notebook keys with the addition.
63+
64+
#### Required Field
65+
66+
The `id` field in cells would _always_ be **required** for any future nbformat versions (4.5+). In contrast to an *optional* field, the required field avoids applications having to conditionally check if an id is present or not.
67+
68+
Relaxing the field to *optional* would lead to undesirable behavior. An optional field would lead to partial implementation in applications and difficulty in having consistent experiences with build on top of the id change.
69+
70+
#### Reason for Character Restrictions (pattern, min/max length)
71+
72+
The [RFC 3986 (Uniform Resource Identifier (URI): Generic Syntax)](https://www.ietf.org/rfc/rfc3986.txt) defines the unreserved characters allowed for URI generation. Since IDs should be usable as referencable points in web requests, we want to restrict characters to at least these characters. Of these remaining non-alphanumeric reserved characters (`-`, `.`, `_`, and `~`) three of them have semantic meaning or are restricted in URL generation leaving only alphanumeric and `-` as legal characters we want to support. This extra restriction also helps with storage of ids in databases, where non-ascii characters in identifiers can oftentimes lead to query, storage, or application bugs when not handled correctly. Since we don't have a pre-existing strong need for such characters (`.`, `_`, and `~`) in our `id` field, we propose not introducing the additional complexity of allowing these other characters here.
73+
74+
The length restrictions are there for a few reasons. First, you don't want empty strings in your ids, so enforce some natural minimum. We could use 1 or 2 for accepting bascially any id pattern, or be more restrictive with a higher minimum to reserve a wider combination of min length ids (`63^k` combinations). Second, you want a fixed max length for string identifiers for indexable ids in many database solutions for both performance and ease of implemntation concerns. These will certainly be used in recall mechanisms so ease of database use should be a strong criterian. Third, a UUID string takes 36 characters to represent (with the `-` characters), and we likely want to support this as a supported identity pattern for certain applications that want this.
75+
76+
### Updating older formats
77+
78+
Older formats can be loaded by nbformat and trivially updated to 4.5 format by running `str(uuid.uuid4())[:8]` to populate the new id field. See the [Case: loading notebook without cell id](#Case-loading-notebook-without-cell-id) section for more options for auto-filling ids.
79+
80+
### Alternative Schema Change
81+
82+
Originally a UUID schema was proposed with:
83+
84+
```
85+
"id": {
86+
"description": "A UUID field representing the identifier of this particular cell.",
87+
"type": "uuid"
88+
}
89+
```
90+
where the `id` field uses the `uuid` type indicator to resolve its value. This is effectively a more restrictive variant of the string regex above. The `uuid` alternative has been dropped as the primary proposed pattern to better support the existing aforementioned `id` generating schemes and to avoid large URI / content generation by direct insertion of the cell id. If `uuid` were adopted instead applications with custom ids would have to do more to migrate existing documents and byte-compression patterns would be needed for shorter URL generation tasks.
91+
92+
The `uuid` type was recently [added to json-schema](https://json-schema.org/draft/2019-09/release-notes.html) referencing [RFC.4122](https://xml2rfc.tools.ietf.org/public/rfc/bibxml/reference.RFC.4122.xml) which is linked for those unfamiliar with it.
93+
94+
As an informational data point, the [jupyterlab-interactive-dashboard-editor](https://github.com/jupytercalpoly/jupyterlab-interactive-dashboard-editor/tree/master/src) uses UUID for their cell ID.
95+
96+
### Reference implementation
97+
98+
The nbformat [PR#189](https://github.com/jupyter/nbformat/pull/189) has a full (unreviewed) working change of the proposal applied to nbformat. Outside of tests and the cell id uniqueness check the change can be captured with this diff:
99+
100+
```
101+
diff --git a/nbformat/v4/nbformat.v4.schema.json b/nbformat/v4/nbformat.v4.schema.json
102+
index e3dedf2..4f192e6 100644
103+
--- a/nbformat/v4/nbformat.v4.schema.json
104+
+++ b/nbformat/v4/nbformat.v4.schema.json
105+
@@ -1,6 +1,6 @@
106+
{
107+
"$schema": "http://json-schema.org/draft-04/schema#",
108+
- "description": "Jupyter Notebook v4.4 JSON schema.",
109+
+ "description": "Jupyter Notebook v4.5 JSON schema.",
110+
"type": "object",
111+
"additionalProperties": false,
112+
"required": ["metadata", "nbformat_minor", "nbformat", "cells"],
113+
@@ -98,6 +98,14 @@
114+
},
115+
116+
"definitions": {
117+
+ "cell_id": {
118+
+ "description": "A string field representing the identifier of this particular cell.",
119+
+ "type": "string",
120+
+ "pattern": "^[a-zA-Z0-9-]+$",
121+
+ "minLength": 2,
122+
+ "maxLength": 36
123+
+ },
124+
+
125+
"cell": {
126+
"type": "object",
127+
"oneOf": [
128+
@@ -111,8 +119,9 @@
129+
"description": "Notebook raw nbconvert cell.",
130+
"type": "object",
131+
"additionalProperties": false,
132+
- "required": ["cell_type", "metadata", "source"],
133+
+ "required": ["id", "cell_type", "metadata", "source"],
134+
"properties": {
135+
+ "id": {"$ref": "#/definitions/cell_id"},
136+
"cell_type": {
137+
"description": "String identifying the type of cell.",
138+
"enum": ["raw"]
139+
@@ -148,8 +157,9 @@
140+
"description": "Notebook markdown cell.",
141+
"type": "object",
142+
"additionalProperties": false,
143+
- "required": ["cell_type", "metadata", "source"],
144+
+ "required": ["id", "cell_type", "metadata", "source"],
145+
"properties": {
146+
+ "id": {"$ref": "#/definitions/cell_id"},
147+
"cell_type": {
148+
"description": "String identifying the type of cell.",
149+
"enum": ["markdown"]
150+
@@ -181,8 +191,9 @@
151+
"description": "Notebook code cell.",
152+
"type": "object",
153+
"additionalProperties": false,
154+
- "required": ["cell_type", "metadata", "source", "outputs", "execution_count"],
155+
+ "required": ["id", "cell_type", "metadata", "source", "outputs", "execution_count"],
156+
"properties": {
157+
+ "id": {"$ref": "#/definitions/cell_id"},
158+
"cell_type": {
159+
"description": "String identifying the type of cell.",
160+
"enum": ["code"]
161+
```
162+
163+
## Recommended Application / Usage of ID field
164+
165+
1. Applications should manage id fields as they wish within the rules if they want to have consistent id patterns.
166+
2. Applications that don't care should use the default id generation of the underlying notebook save/load mechanisms.
167+
3. When loading from older formats, cell ids should be filled out with a unique value.
168+
4. UUIDs are one valid, simple way of ensuring uniqueness, but not necessary.
169+
- Lots of large random strings in notebooks can be frustrating
170+
- 128-bit UUIDs are also vast overkill for the level of uniqueness we need within a notebook with <1000 candidates for collisions. They make for opaque URLs, noise in the files, etc
171+
- Human-readable strings are preferable defaults for ids that will be used in links / visible
172+
5. Uniqueness across notebooks is not a goal.
173+
- A managed ecosystem might make use of uniqueness across documents, but the spec doesn't expect this behavior
174+
6. Users should not need to directly view or edit cell ids.
175+
- Applications need not make any user interface changes to support the 4.5 format with ids added. If they wish to display cell ids they can but generally they should be invisible to the end user unless they're programmatically referencing a cell.
176+
177+
### Case: loading notebook without cell id
178+
179+
#### Option A: strings from an integer counter
180+
181+
A valid strategy, when populating cell ids from a notebook on import from another id-less source or older format version, to use e.g. strings from an integer counter.
182+
183+
In fact, if an editor app keeps track of current cell ids, the following strategy ensures uniqueness:
184+
185+
```python
186+
cell_id_counter = 0
187+
existing_cell_ids = set()
188+
189+
def get_cell_id(cell_id=None):
190+
"""Return a new unique cell id
191+
192+
if cell_id is given, use it if available (e.g. preserving cell id on paste, while ensuring no collisions)
193+
"""
194+
global cell_id_counter
195+
196+
if cell_id and cell_id not in existing_cell_ids:
197+
# requested cell id is available
198+
existing_cell_ids.add(cell_id)
199+
return cell_id
200+
201+
# generate new unique id
202+
cell_id = f"id{cell_id_counter}"
203+
while cell_id in existing_cell_ids:
204+
cell_id_counter += 1
205+
cell_id = f"id{cell_id_counter}"
206+
existing_cell_ids.add(cell_id)
207+
cell_id_counter += 1
208+
return cell_id
209+
210+
def free_cell_id(cell_id):
211+
"""record that a cell id is no longer in use"""
212+
existing_cell_ids.remove(cell_id)
213+
```
214+
215+
#### Option B: 64-bit random id
216+
217+
If bookkeeping of current cell ids is not desirable, a 64-bit random id (11 chars without padding in b64) has a 10^-14 chance of collisions on 1000 cells, while an 8-char b64 string (48b) is still 10^-9.
218+
219+
```python
220+
def get_cell_id(id_length=8):
221+
# Ok technically this isn't exactly a 64-bit k-length string... but it's close and easy to implement
222+
return str(uuid.uuid4())[:id_length]
223+
```
224+
225+
#### Option C: Join human-readable strings from a corpus randomly
226+
227+
One frequently used pattern for generating human recognizable ids is to combine common words together instead of arbitrarily random bits. Things like `danger-noodle` is a lot easier to remember or reference for a person than `ZGFuZ2VyLW5vb2RsZQ==`. Below would be how this is achieved, though it requires a set of names to use in id generation. There's dependencies in Python, as well as corpus csv files, for this that make it convienent but it would have to add to the install depenencies.
228+
229+
```python
230+
def get_cell_id(num_words=2):
231+
return "-".join(random.sample(name_corpus, num_words))
232+
```
233+
234+
#### Preference
235+
236+
Use Option B. Option C is also viable but adds a corpus requirement to the id generation step.
237+
238+
## Questions
239+
240+
1. How is splitting cells handled?
241+
- One cell (second part of the split) gets a new cell ID
242+
2. What if I copy and paste (surely you do not want duplicate ids...)
243+
- On paste give the pasted cell a different ID if there's already one with the same ID as being pasted. The copied cell should have a new id
244+
3. What if you cut-paste (surely you want to keep the id)?
245+
- On paste give the pasted cell a different ID if there's already one with the same ID as being pasted. For cut this means the id can be preserved because there's no conflict on resolution of the move action
246+
4. What if you cut-paste, and paste a second time?
247+
- On paste give the pasted cell a different ID if there's already one with the same ID as being pasted. In this case the second paste needs a new id
248+
5. How should loaders handle notebook loading errors?
249+
- On notebook load, if an older format update and fill in ids. If an invalid id format for a 4.5+ file, then raise a validation error like we do for other schema errors. We could auto-correct for bad ids if that's deemed appropriate.
250+
6. Would cell ID be changed if the cell content changes, or just created one time when the cell is created? As an extreme example: What if the content of the cell is cut out entirely and pasted into a new cell? My assumption is the ID would remain the same, right?
251+
- Correct. It stays the same once created.
252+
7. So if nbformat >= 4.5 loads in a pre 4.5 notebook, then a cell ID would be generated and added to each cell?
253+
- Yes.
254+
8. If a cell is cut out of a notebook and pasted into another, should the cell ID be retained?
255+
- No. Much like copying contents out of one document into another -- you have a new cell with equivalent contends and a new id.
256+
9. What are the details when splitting cells?
257+
- One cell (preferably the one with the top half of the code) keeps the id, the other gets a new id. This could be adjusted if folks want a different behavior without being a huge problem so long as we're consistent.
258+
259+
## Pros and Cons
260+
261+
Pros associated with this implementation include:
262+
263+
- Enables scenarios that require us to reason about cells as if they were independent entities
264+
- Used by Colab, among others, for many many years, and it is generally useful. This JEP would standardize to minimize fragmentation and differing approaches.
265+
- Allows apps that want to reference specific cells within a notebook
266+
- Makes reasoning about cells unambiguous (e.g. associate comments to a cell)
267+
268+
Cons associated with this implementation include:
269+
270+
- lack of UUID and a "notebook-only" uniqueness guarantee makes merging two notebooks difficult without managing the ids so they remain unique in the resulting notebook
271+
- applications have to add default ID generation if not using nbformat (or not python) for this (took 1 hour to add the proposal PR to nbformat with tests included)
272+
273+
## Relevant Issues, PR, and discussion
274+
275+
Pre-proposal discussion:
276+
277+
- [JEP issue #61: Proposal: 4.5 Format Cell ID](https://github.com/jupyter/enhancement-proposals/issues/61)
278+
- [Notes from JEP Draft: Cell ID/Information Bi-weekly Meeting](https://hackmd.io/AkuHK5lPQ5-0BBTF8-SPzQ?view)
279+
- [nbformat PR#189: Adds this proposal to nbformat](https://github.com/jupyter/nbformat/pull/189)
280+
281+
Out of scope for this proposal (notebook ID):
282+
283+
- [nbformat issue #148: Adding unique ID to the notebook metadata](https://github.com/jupyter/nbformat/issues/148)
284+
285+
## Interested
286+
287+
@MSeal, @ellisonbg, @minrk, @jasongrout, @takluyver, @Carreau, @rgbkrk, @choldgraf, @SylvainCorlay, @willingc, @captainsafia, @ivanov, @yuvipanda, @bollwvyl, @blois, @betatim, @echarles, @tonyfast
288+
289+
---
290+
291+
# Appendix 1: Additional Information
292+
293+
In this JEP, we have tried to address the majority of comments made during the
294+
pre-proposal period. This appendix highlights this feedback and additional items.
295+
296+
## Pre-proposal Feedback
297+
298+
Feedback can be found in the pre-proposal discussions listed [above](#Relevant-Issues-PR-and-discussion). Additional feedback can be found in [Notes from JEP Draft: Cell ID/Information Bi-weekly Meeting](https://hackmd.io/AkuHK5lPQ5-0BBTF8-SPzQ?view).
299+
300+
[Min's detailed feedback](https://github.com/jupyter/enhancement-proposals/issues/61#issuecomment-672752443) was taken and incorporated into the JEP.
301+
302+
### $id ref Conclusion
303+
304+
We had a follow-up conversation with Nick Bollweg and Tony Fast about JSON schema and JSON-LD. In the course of the bi-weekly meeting, we discussed $id ref. From further review of how the [\$id property](https://json-schema.org/understanding-json-schema/structuring.html#the-id-property) works in JSON Schema we determined that the use for this flag is orthogonal to actual proposed usecase presented here. A future JEP may choose to pursue using this field for another use in the future, but we're going to keep it out of scope for this JEP.
305+
306+
## Implementation Question
307+
308+
### Auto-Update
309+
310+
A decision should be made to determine whether or not to auto-update older notebook formats to 4.5. Our recommendation would be to auto-update to 4.5.
311+
312+
### Auto-Fill on Save
313+
314+
In the event of a content save for 4.5 with no id, we can either raise a ValidationError (as the example PR does right now) or auto-fill the missing id with a randomly generated id. We'd prefer the latter pattern, provided that given invalid ids still raise a ValidationError.

0 commit comments

Comments
 (0)