Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
06bd645177
commit
3fdeaff80e
|
@ -1495,6 +1495,12 @@
|
|||
changes: ["vendor/gems/mail-smtp_pool/**/*"]
|
||||
- <<: *if-merge-request-labels-run-all-rspec
|
||||
|
||||
.vendor:rules:ipynbdiff:
|
||||
rules:
|
||||
- <<: *if-merge-request
|
||||
changes: ["vendor/gems/ipynbdiff/**/*"]
|
||||
- <<: *if-merge-request-labels-run-all-rspec
|
||||
|
||||
##################
|
||||
# Releases rules #
|
||||
##################
|
||||
|
|
|
@ -5,3 +5,10 @@ vendor mail-smtp_pool:
|
|||
trigger:
|
||||
include: vendor/gems/mail-smtp_pool/.gitlab-ci.yml
|
||||
strategy: depend
|
||||
vendor ipynbdiff:
|
||||
extends:
|
||||
- .vendor:rules:ipynbdiff
|
||||
needs: []
|
||||
trigger:
|
||||
include: vendor/gems/ipynbdiff/.gitlab-ci.yml
|
||||
strategy: depend
|
||||
|
|
2
Gemfile
2
Gemfile
|
@ -546,6 +546,6 @@ gem 'ipaddress', '~> 0.8.3'
|
|||
|
||||
gem 'parslet', '~> 1.8'
|
||||
|
||||
gem 'ipynbdiff', '0.4.7'
|
||||
gem 'ipynbdiff', path: 'vendor/gems/ipynbdiff'
|
||||
|
||||
gem 'ed25519', '~> 1.3.0'
|
||||
|
|
12
Gemfile.lock
12
Gemfile.lock
|
@ -1,3 +1,10 @@
|
|||
PATH
|
||||
remote: vendor/gems/ipynbdiff
|
||||
specs:
|
||||
ipynbdiff (0.4.7)
|
||||
diffy (~> 3.3)
|
||||
json (~> 2.5, >= 2.5.1)
|
||||
|
||||
PATH
|
||||
remote: vendor/gems/mail-smtp_pool
|
||||
specs:
|
||||
|
@ -667,9 +674,6 @@ GEM
|
|||
invisible_captcha (1.1.0)
|
||||
rails (>= 4.2)
|
||||
ipaddress (0.8.3)
|
||||
ipynbdiff (0.4.7)
|
||||
diffy (~> 3.3)
|
||||
json (~> 2.5, >= 2.5.1)
|
||||
jaeger-client (1.1.0)
|
||||
opentracing (~> 0.3)
|
||||
thrift
|
||||
|
@ -1575,7 +1579,7 @@ DEPENDENCIES
|
|||
icalendar
|
||||
invisible_captcha (~> 1.1.0)
|
||||
ipaddress (~> 0.8.3)
|
||||
ipynbdiff (= 0.4.7)
|
||||
ipynbdiff!
|
||||
jira-ruby (~> 2.1.4)
|
||||
js_regex (~> 3.7)
|
||||
json (~> 2.5.1)
|
||||
|
|
|
@ -1,11 +1,5 @@
|
|||
<script>
|
||||
import {
|
||||
GlSafeHtmlDirective as SafeHtml,
|
||||
GlModal,
|
||||
GlToast,
|
||||
GlTooltip,
|
||||
GlModalDirective,
|
||||
} from '@gitlab/ui';
|
||||
import { GlSafeHtmlDirective as SafeHtml, GlToast, GlTooltip, GlModalDirective } from '@gitlab/ui';
|
||||
import $ from 'jquery';
|
||||
import Sortable from 'sortablejs';
|
||||
import Vue from 'vue';
|
||||
|
@ -20,11 +14,16 @@ import { getSortableDefaultOptions, isDragging } from '~/sortable/utils';
|
|||
import TaskList from '~/task_list';
|
||||
import Tracking from '~/tracking';
|
||||
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
|
||||
import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
|
||||
import createWorkItemFromTaskMutation from '~/work_items/graphql/create_work_item_from_task.mutation.graphql';
|
||||
|
||||
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
|
||||
import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
|
||||
import CreateWorkItem from '~/work_items/pages/create_work_item.vue';
|
||||
import {
|
||||
TRACKING_CATEGORY_SHOW,
|
||||
TASK_TYPE_NAME,
|
||||
WIDGET_TYPE_DESCRIPTION,
|
||||
} from '~/work_items/constants';
|
||||
import animateMixin from '../mixins/animate';
|
||||
import { convertDescriptionWithNewSort } from '../utils';
|
||||
|
||||
|
@ -40,12 +39,11 @@ export default {
|
|||
GlModal: GlModalDirective,
|
||||
},
|
||||
components: {
|
||||
GlModal,
|
||||
CreateWorkItem,
|
||||
GlTooltip,
|
||||
WorkItemDetailModal,
|
||||
},
|
||||
mixins: [animateMixin, glFeatureFlagMixin(), Tracking.mixin()],
|
||||
inject: ['fullPath'],
|
||||
props: {
|
||||
canUpdate: {
|
||||
type: Boolean,
|
||||
|
@ -103,6 +101,7 @@ export default {
|
|||
workItemId: isPositiveInteger(workItemId)
|
||||
? convertToGraphQLId(TYPE_WORK_ITEM, workItemId)
|
||||
: undefined,
|
||||
workItemTypes: [],
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
|
@ -117,11 +116,28 @@ export default {
|
|||
return !this.workItemId || !this.workItemsEnabled;
|
||||
},
|
||||
},
|
||||
workItemTypes: {
|
||||
query: projectWorkItemTypesQuery,
|
||||
variables() {
|
||||
return {
|
||||
fullPath: this.fullPath,
|
||||
};
|
||||
},
|
||||
update(data) {
|
||||
return data.workspace?.workItemTypes?.nodes;
|
||||
},
|
||||
skip() {
|
||||
return !this.workItemsEnabled;
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
workItemsEnabled() {
|
||||
return this.glFeatures.workItems;
|
||||
},
|
||||
taskWorkItemType() {
|
||||
return this.workItemTypes.find((type) => type.name === TASK_TYPE_NAME)?.id;
|
||||
},
|
||||
issueGid() {
|
||||
return this.issueId ? convertToGraphQLId(TYPE_WORK_ITEM, this.issueId) : null;
|
||||
},
|
||||
|
@ -344,8 +360,8 @@ export default {
|
|||
<use href="${gon.sprite_icons}#doc-new"></use>
|
||||
</svg>
|
||||
`;
|
||||
button.setAttribute('aria-label', s__('WorkItem|Convert to work item'));
|
||||
button.addEventListener('click', () => this.openCreateTaskModal(button));
|
||||
button.setAttribute('aria-label', s__('WorkItem|Create task'));
|
||||
button.addEventListener('click', () => this.handleCreateTask(button));
|
||||
this.insertButtonNextToTaskText(item, button);
|
||||
});
|
||||
},
|
||||
|
@ -386,17 +402,11 @@ export default {
|
|||
lineNumberEnd: lineNumbers[1],
|
||||
};
|
||||
},
|
||||
openCreateTaskModal(el) {
|
||||
this.setActiveTask(el);
|
||||
this.$refs.modal.show();
|
||||
},
|
||||
closeCreateTaskModal() {
|
||||
this.$refs.modal.hide();
|
||||
},
|
||||
openWorkItemDetailModal(el) {
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setActiveTask(el);
|
||||
this.$refs.detailsModal.show();
|
||||
},
|
||||
|
@ -404,9 +414,54 @@ export default {
|
|||
this.workItemId = undefined;
|
||||
this.updateWorkItemIdUrlQuery(undefined);
|
||||
},
|
||||
handleCreateTask(description) {
|
||||
this.$emit('updateDescription', description);
|
||||
this.closeCreateTaskModal();
|
||||
async handleCreateTask(el) {
|
||||
this.setActiveTask(el);
|
||||
try {
|
||||
const { data } = await this.$apollo.mutate({
|
||||
mutation: createWorkItemFromTaskMutation,
|
||||
variables: {
|
||||
input: {
|
||||
id: this.issueGid,
|
||||
workItemData: {
|
||||
lockVersion: this.lockVersion,
|
||||
title: this.activeTask.title,
|
||||
lineNumberStart: Number(this.activeTask.lineNumberStart),
|
||||
lineNumberEnd: Number(this.activeTask.lineNumberEnd),
|
||||
workItemTypeId: this.taskWorkItemType,
|
||||
},
|
||||
},
|
||||
},
|
||||
update(store, { data: { workItemCreateFromTask } }) {
|
||||
const { newWorkItem } = workItemCreateFromTask;
|
||||
|
||||
store.writeQuery({
|
||||
query: workItemQuery,
|
||||
variables: {
|
||||
id: newWorkItem.id,
|
||||
},
|
||||
data: {
|
||||
workItem: newWorkItem,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { workItem, newWorkItem } = data.workItemCreateFromTask;
|
||||
|
||||
const updatedDescription = workItem?.widgets?.find(
|
||||
(widget) => widget.type === WIDGET_TYPE_DESCRIPTION,
|
||||
)?.descriptionHtml;
|
||||
|
||||
this.$emit('updateDescription', updatedDescription);
|
||||
this.workItemId = newWorkItem.id;
|
||||
this.openWorkItemDetailModal(el);
|
||||
} catch (error) {
|
||||
createFlash({
|
||||
message: s__('WorkItem|Something went wrong when creating a work item. Please try again'),
|
||||
error,
|
||||
captureError: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
handleDeleteTask(description) {
|
||||
this.$emit('updateDescription', description);
|
||||
|
@ -452,19 +507,6 @@ export default {
|
|||
data-testid="textarea"
|
||||
>
|
||||
</textarea>
|
||||
|
||||
<gl-modal ref="modal" size="lg" modal-id="create-task-modal" hide-footer body-class="gl-p-0!">
|
||||
<create-work-item
|
||||
is-modal
|
||||
:initial-title="activeTask.title"
|
||||
:issue-gid="issueGid"
|
||||
:lock-version="lockVersion"
|
||||
:line-number-start="activeTask.lineNumberStart"
|
||||
:line-number-end="activeTask.lineNumberEnd"
|
||||
@closeModal="closeCreateTaskModal"
|
||||
@onCreate="handleCreateTask"
|
||||
/>
|
||||
</gl-modal>
|
||||
<work-item-detail-modal
|
||||
ref="detailsModal"
|
||||
:can-update="canUpdate"
|
||||
|
@ -478,7 +520,7 @@ export default {
|
|||
/>
|
||||
<template v-if="workItemsEnabled">
|
||||
<gl-tooltip v-for="item in taskButtons" :key="item" :target="item">
|
||||
{{ s__('WorkItem|Convert to work item') }}
|
||||
{{ s__('WorkItem|Create task') }}
|
||||
</gl-tooltip>
|
||||
</template>
|
||||
</div>
|
||||
|
|
|
@ -54,7 +54,7 @@ export default {
|
|||
:label-for="$options.labelId"
|
||||
label-cols="3"
|
||||
label-cols-lg="2"
|
||||
label-class="gl-pb-0!"
|
||||
label-class="gl-pb-0! gl-overflow-wrap-break"
|
||||
class="gl-align-items-center"
|
||||
>
|
||||
<gl-form-select
|
||||
|
|
|
@ -40,7 +40,7 @@ export default {
|
|||
|
||||
<template>
|
||||
<h2
|
||||
class="gl-font-weight-normal gl-sm-font-weight-bold gl-my-5 gl-w-full"
|
||||
class="gl-font-weight-normal gl-sm-font-weight-bold gl-mb-5 gl-mt-0 gl-w-full"
|
||||
:class="{ 'gl-cursor-not-allowed': disabled }"
|
||||
aria-labelledby="item-title"
|
||||
>
|
||||
|
|
|
@ -137,17 +137,19 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gl-display-flex gl-mb-4 work-item-assignees gl-relative">
|
||||
<span class="gl-font-weight-bold gl-w-15 gl-pt-2" data-testid="assignees-title">{{
|
||||
assigneeText
|
||||
}}</span>
|
||||
<div class="form-row gl-mb-5 work-item-assignees gl-relative">
|
||||
<span
|
||||
class="gl-font-weight-bold col-lg-2 col-3 gl-pt-2 min-w-fit-content gl-overflow-wrap-break"
|
||||
data-testid="assignees-title"
|
||||
>{{ assigneeText }}</span
|
||||
>
|
||||
<gl-token-selector
|
||||
ref="tokenSelector"
|
||||
v-model="localAssignees"
|
||||
:container-class="containerClass"
|
||||
class="gl-flex-grow-1 gl-border gl-border-white gl-hover-border-gray-200 gl-rounded-base col-9 gl-align-self-start"
|
||||
:dropdown-items="searchUsers"
|
||||
:loading="isLoading"
|
||||
class="gl-w-full gl-border gl-border-white gl-hover-border-gray-200 gl-rounded-base"
|
||||
@input="focusTokenSelector"
|
||||
@text-input="debouncedSearchKeyUpdate"
|
||||
@focus="handleFocus"
|
||||
|
|
|
@ -35,7 +35,7 @@ export default {
|
|||
isEditing: false,
|
||||
isSubmitting: false,
|
||||
isSubmittingWithKeydown: false,
|
||||
desc: '',
|
||||
descriptionText: '',
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
|
@ -71,16 +71,17 @@ export default {
|
|||
descriptionHtml() {
|
||||
return this.workItemDescription?.descriptionHtml;
|
||||
},
|
||||
descriptionText: {
|
||||
get() {
|
||||
return this.desc;
|
||||
},
|
||||
set(desc) {
|
||||
this.desc = desc;
|
||||
},
|
||||
descriptionEmpty() {
|
||||
return this.descriptionHtml?.trim() === '';
|
||||
},
|
||||
workItemDescription() {
|
||||
return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_DESCRIPTION);
|
||||
const descriptionWidget = this.workItem?.widgets?.find(
|
||||
(widget) => widget.type === WIDGET_TYPE_DESCRIPTION,
|
||||
);
|
||||
return {
|
||||
...descriptionWidget,
|
||||
description: descriptionWidget?.description || '',
|
||||
};
|
||||
},
|
||||
workItemType() {
|
||||
return this.workItem?.workItemType?.name;
|
||||
|
@ -95,14 +96,14 @@ export default {
|
|||
async startEditing() {
|
||||
this.isEditing = true;
|
||||
|
||||
this.desc = getDraft(this.autosaveKey) || this.workItemDescription?.description || '';
|
||||
this.descriptionText = getDraft(this.autosaveKey) || this.workItemDescription?.description;
|
||||
|
||||
await this.$nextTick();
|
||||
|
||||
this.$refs.textarea.focus();
|
||||
},
|
||||
async cancelEditing() {
|
||||
const isDirty = this.desc !== this.workItemDescription?.description;
|
||||
const isDirty = this.descriptionText !== this.workItemDescription?.description;
|
||||
|
||||
if (isDirty) {
|
||||
const msg = s__('WorkItem|Are you sure you want to cancel editing?');
|
||||
|
@ -125,7 +126,7 @@ export default {
|
|||
return;
|
||||
}
|
||||
|
||||
updateDraft(this.autosaveKey, this.desc);
|
||||
updateDraft(this.autosaveKey, this.descriptionText);
|
||||
},
|
||||
async updateWorkItem(event) {
|
||||
if (event.key) {
|
||||
|
@ -171,25 +172,10 @@ export default {
|
|||
<template>
|
||||
<gl-form-group
|
||||
v-if="isEditing"
|
||||
class="gl-pt-5 gl-mb-5 gl-mt-0! gl-border-t! gl-border-b"
|
||||
class="gl-my-5"
|
||||
:label="__('Description')"
|
||||
label-for="work-item-description"
|
||||
label-class="gl-float-left"
|
||||
>
|
||||
<div class="gl-display-flex gl-justify-content-flex-end">
|
||||
<gl-button class="gl-ml-auto" data-testid="cancel" @click="cancelEditing">{{
|
||||
__('Cancel')
|
||||
}}</gl-button>
|
||||
<gl-button
|
||||
class="js-no-auto-disable gl-ml-4"
|
||||
category="primary"
|
||||
variant="confirm"
|
||||
:loading="isSubmitting"
|
||||
data-testid="save-description"
|
||||
@click="updateWorkItem"
|
||||
>{{ __('Save') }}</gl-button
|
||||
>
|
||||
</div>
|
||||
<markdown-field
|
||||
can-attach-file
|
||||
:textarea-value="descriptionText"
|
||||
|
@ -216,19 +202,35 @@ export default {
|
|||
></textarea>
|
||||
</template>
|
||||
</markdown-field>
|
||||
</gl-form-group>
|
||||
<div v-else class="gl-pt-5 gl-mb-5 gl-border-t gl-border-b">
|
||||
|
||||
<div class="gl-display-flex">
|
||||
<h3 class="gl-font-base gl-mt-0">{{ __('Description') }}</h3>
|
||||
<gl-button
|
||||
category="primary"
|
||||
variant="confirm"
|
||||
:loading="isSubmitting"
|
||||
data-testid="save-description"
|
||||
@click="updateWorkItem"
|
||||
>{{ __('Save') }}</gl-button
|
||||
>
|
||||
<gl-button category="tertiary" class="gl-ml-3" data-testid="cancel" @click="cancelEditing">{{
|
||||
__('Cancel')
|
||||
}}</gl-button>
|
||||
</div>
|
||||
</gl-form-group>
|
||||
<div v-else class="gl-mb-5">
|
||||
<div class="gl-display-flex gl-align-items-center gl-mb-5">
|
||||
<h3 class="gl-font-base gl-my-0">{{ __('Description') }}</h3>
|
||||
<gl-button
|
||||
v-if="canEdit"
|
||||
class="gl-ml-auto"
|
||||
icon="pencil"
|
||||
data-testid="edit-description"
|
||||
:aria-label="__('Edit')"
|
||||
@click="startEditing"
|
||||
>{{ __('Edit') }}</gl-button
|
||||
>
|
||||
/>
|
||||
</div>
|
||||
<div v-safe-html="descriptionHtml" class="md gl-mb-5"></div>
|
||||
|
||||
<div v-if="descriptionEmpty" class="gl-text-secondary gl-mb-5">{{ __('None') }}</div>
|
||||
<div v-else v-safe-html="descriptionHtml" class="md gl-mb-5 gl-min-h-8"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -101,7 +101,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<section>
|
||||
<section class="gl-pt-5">
|
||||
<gl-alert v-if="error" variant="danger" @dismiss="error = undefined">
|
||||
{{ error }}
|
||||
</gl-alert>
|
||||
|
@ -113,6 +113,7 @@ export default {
|
|||
</gl-skeleton-loader>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="gl-font-weight-bold gl-text-secondary gl-mb-2">{{ workItemType }}</div>
|
||||
<div class="gl-display-flex gl-align-items-start">
|
||||
<work-item-title
|
||||
:work-item-id="workItem.id"
|
||||
|
@ -125,11 +126,16 @@ export default {
|
|||
<work-item-actions
|
||||
:work-item-id="workItem.id"
|
||||
:can-delete="canDelete"
|
||||
class="gl-ml-auto gl-mt-6"
|
||||
class="gl-mt-4"
|
||||
@deleteWorkItem="$emit('deleteWorkItem')"
|
||||
@error="error = $event"
|
||||
/>
|
||||
</div>
|
||||
<work-item-state
|
||||
:work-item="workItem"
|
||||
:work-item-parent-id="workItemParentId"
|
||||
@error="error = $event"
|
||||
/>
|
||||
<template v-if="workItemsMvc2Enabled">
|
||||
<work-item-assignees
|
||||
v-if="workItemAssignees"
|
||||
|
@ -138,14 +144,10 @@ export default {
|
|||
/>
|
||||
<work-item-weight v-if="workItemWeight" :weight="workItemWeight.weight" />
|
||||
</template>
|
||||
<work-item-state
|
||||
:work-item="workItem"
|
||||
:work-item-parent-id="workItemParentId"
|
||||
@error="error = $event"
|
||||
/>
|
||||
<work-item-description
|
||||
v-if="hasDescriptionWidget"
|
||||
:work-item-id="workItem.id"
|
||||
class="gl-pt-5"
|
||||
@error="error = $event"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
@ -104,7 +104,6 @@ export default {
|
|||
size="lg"
|
||||
modal-id="work-item-detail-modal"
|
||||
header-class="gl-p-0 gl-pb-2!"
|
||||
body-class="gl-pb-6!"
|
||||
@hide="closeModal"
|
||||
>
|
||||
<gl-alert v-if="error" variant="danger" @dismiss="error = false">
|
||||
|
|
|
@ -19,8 +19,10 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="hasIssueWeightsFeature" class="gl-mb-5">
|
||||
<span class="gl-display-inline-block gl-font-weight-bold gl-w-15">{{ __('Weight') }}</span>
|
||||
{{ weightText }}
|
||||
<div v-if="hasIssueWeightsFeature" class="gl-mb-5 form-row">
|
||||
<span class="gl-font-weight-bold col-lg-2 col-3 gl-overflow-wrap-break">{{
|
||||
__('Weight')
|
||||
}}</span>
|
||||
<span class="gl-ml-5">{{ weightText }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -13,7 +13,7 @@ export const i18n = {
|
|||
updateError: s__('WorkItem|Something went wrong while updating the work item. Please try again.'),
|
||||
};
|
||||
|
||||
export const DEFAULT_MODAL_TYPE = 'Task';
|
||||
export const TASK_TYPE_NAME = 'Task';
|
||||
|
||||
export const WIDGET_TYPE_ASSIGNEE = 'ASSIGNEES';
|
||||
export const WIDGET_TYPE_DESCRIPTION = 'DESCRIPTION';
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
#import "./work_item.fragment.graphql"
|
||||
|
||||
mutation workItemCreateFromTask($input: WorkItemCreateFromTaskInput!) {
|
||||
workItemCreateFromTask(input: $input) {
|
||||
workItem {
|
||||
id
|
||||
descriptionHtml
|
||||
...WorkItem
|
||||
}
|
||||
newWorkItem {
|
||||
...WorkItem
|
||||
}
|
||||
errors
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ import workItemQuery from '../graphql/work_item.query.graphql';
|
|||
import createWorkItemMutation from '../graphql/create_work_item.mutation.graphql';
|
||||
import createWorkItemFromTaskMutation from '../graphql/create_work_item_from_task.mutation.graphql';
|
||||
import projectWorkItemTypesQuery from '../graphql/project_work_item_types.query.graphql';
|
||||
import { DEFAULT_MODAL_TYPE } from '../constants';
|
||||
|
||||
import ItemTitle from '../components/item_title.vue';
|
||||
|
||||
|
@ -24,11 +23,6 @@ export default {
|
|||
},
|
||||
inject: ['fullPath'],
|
||||
props: {
|
||||
isModal: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
initialTitle: {
|
||||
type: String,
|
||||
required: false,
|
||||
|
@ -78,13 +72,6 @@ export default {
|
|||
text: node.name,
|
||||
}));
|
||||
},
|
||||
result() {
|
||||
if (!this.selectedWorkItemType && this.isModal) {
|
||||
this.selectedWorkItemType = this.formOptions.find(
|
||||
(options) => options.text === DEFAULT_MODAL_TYPE,
|
||||
)?.value;
|
||||
}
|
||||
},
|
||||
error() {
|
||||
this.error = this.$options.fetchTypesErrorText;
|
||||
},
|
||||
|
@ -104,11 +91,7 @@ export default {
|
|||
methods: {
|
||||
async createWorkItem() {
|
||||
this.loading = true;
|
||||
if (this.isModal) {
|
||||
await this.createWorkItemFromTask();
|
||||
} else {
|
||||
await this.createStandaloneWorkItem();
|
||||
}
|
||||
await this.createStandaloneWorkItem();
|
||||
this.loading = false;
|
||||
},
|
||||
async createStandaloneWorkItem() {
|
||||
|
@ -174,11 +157,7 @@ export default {
|
|||
this.title = title;
|
||||
},
|
||||
handleCancelClick() {
|
||||
if (!this.isModal) {
|
||||
this.$router.go(-1);
|
||||
return;
|
||||
}
|
||||
this.$emit('closeModal');
|
||||
this.$router.go(-1);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -187,7 +166,7 @@ export default {
|
|||
<template>
|
||||
<form @submit.prevent="createWorkItem">
|
||||
<gl-alert v-if="error" variant="danger" @dismiss="error = null">{{ error }}</gl-alert>
|
||||
<div :class="{ 'gl-px-5': isModal }" data-testid="content">
|
||||
<div data-testid="content">
|
||||
<item-title :title="initialTitle" data-testid="title-input" @title-input="handleTitleInput" />
|
||||
<div>
|
||||
<gl-loading-icon
|
||||
|
@ -203,14 +182,11 @@ export default {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="gl-bg-gray-10 gl-py-5 gl-px-6 gl-mt-4"
|
||||
:class="{ 'gl-display-flex gl-justify-content-end': isModal }"
|
||||
>
|
||||
<div class="gl-bg-gray-10 gl-py-5 gl-px-6 gl-mt-4">
|
||||
<gl-button
|
||||
variant="confirm"
|
||||
:disabled="isButtonDisabled"
|
||||
:class="{ 'gl-mr-3': !isModal }"
|
||||
class="gl-mr-3"
|
||||
:loading="loading"
|
||||
data-testid="create-button"
|
||||
type="submit"
|
||||
|
@ -221,7 +197,6 @@ export default {
|
|||
type="button"
|
||||
data-testid="cancel-button"
|
||||
class="gl-order-n1"
|
||||
:class="{ 'gl-mr-3': isModal }"
|
||||
@click="handleCancelClick"
|
||||
>
|
||||
{{ __('Cancel') }}
|
||||
|
|
|
@ -16,7 +16,8 @@ module Users
|
|||
storage_enforcement_banner_third_enforcement_threshold: 5,
|
||||
storage_enforcement_banner_fourth_enforcement_threshold: 6,
|
||||
preview_user_over_limit_free_plan_alert: 7, # EE-only
|
||||
user_reached_limit_free_plan_alert: 8 # EE-only
|
||||
user_reached_limit_free_plan_alert: 8, # EE-only
|
||||
free_group_limited_alert: 9 # EE-only
|
||||
}
|
||||
|
||||
validates :group, presence: true
|
||||
|
|
|
@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/360927
|
|||
milestone: '15.0'
|
||||
type: development
|
||||
group: group::source code
|
||||
default_enabled: false
|
||||
default_enabled: true
|
||||
|
|
|
@ -146,10 +146,10 @@ Destination is deleted if:
|
|||
## Custom HTTP header values
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/361216) in GitLab 15.1 [with a flag](feature_flags.md) named `streaming_audit_event_headers`. Disabled by default.
|
||||
> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/362941) in GitLab 15.2.
|
||||
> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/362941) in GitLab 15.2.
|
||||
|
||||
FLAG:
|
||||
On self-managed GitLab, by default this feature is not available. To make it available per group, ask an administrator to [enable the feature flag](../administration/feature_flags.md) named `streaming_audit_event_headers`.
|
||||
On self-managed GitLab, by default this feature is available. To hide the feature, ask an administrator to [disable the feature flag](../administration/feature_flags.md) named `streaming_audit_event_headers`.
|
||||
On GitLab.com, this feature is available.
|
||||
|
||||
Each streaming destination can have up to 20 custom HTTP headers included with each streamed event.
|
||||
|
|
|
@ -11520,7 +11520,7 @@ Represents an external resource to send audit events to.
|
|||
| ---- | ---- | ----------- |
|
||||
| <a id="externalauditeventdestinationdestinationurl"></a>`destinationUrl` | [`String!`](#string) | External destination to send audit events to. |
|
||||
| <a id="externalauditeventdestinationgroup"></a>`group` | [`Group!`](#group) | Group the destination belongs to. |
|
||||
| <a id="externalauditeventdestinationheaders"></a>`headers` | [`AuditEventStreamingHeaderConnection!`](#auditeventstreamingheaderconnection) | List of additional HTTP headers sent with each event. Available only when feature flag `streaming_audit_event_headers` is enabled. This flag is disabled by default, because the feature is experimental and is subject to change without notice. (see [Connections](#connections)) |
|
||||
| <a id="externalauditeventdestinationheaders"></a>`headers` | [`AuditEventStreamingHeaderConnection!`](#auditeventstreamingheaderconnection) | List of additional HTTP headers sent with each event. Available only when feature flag `streaming_audit_event_headers` is enabled. This flag is enabled by default. (see [Connections](#connections)) |
|
||||
| <a id="externalauditeventdestinationid"></a>`id` | [`ID!`](#id) | ID of the destination. |
|
||||
| <a id="externalauditeventdestinationverificationtoken"></a>`verificationToken` | [`String!`](#string) | Verification token to validate source of event. |
|
||||
|
||||
|
|
|
@ -32,8 +32,7 @@ to work items and adding custom work item types, visit
|
|||
To create a task:
|
||||
|
||||
1. In an issue description, create a [task list](markdown.md#task-lists).
|
||||
1. Hover over a task item and select **Convert to work item** (**{doc-new}**).
|
||||
1. Confirm or edit the title, and select **Create work item**.
|
||||
1. Hover over a task item and select **Create task** (**{doc-new}**).
|
||||
|
||||
## Edit a task
|
||||
|
||||
|
|
|
@ -6214,6 +6214,12 @@ msgstr ""
|
|||
msgid "Billing|You can begin moving members in %{namespaceName} now. A member loses access to the group when you turn off %{strongStart}In a seat%{strongEnd}. If over 5 members have %{strongStart}In a seat%{strongEnd} enabled after June 22, 2022, we'll select the 5 members who maintain access. We'll first count members that have Owner and Maintainer roles, then the most recently active members until we reach 5 members. The remaining members will get a status of Over limit and lose access to the group."
|
||||
msgstr ""
|
||||
|
||||
msgid "Billing|Your free group is now limited to %{free_user_limit} members"
|
||||
msgstr ""
|
||||
|
||||
msgid "Billing|Your group recently changed to use the Free plan. Free groups are limited to %{free_user_limit} members and the remaining members will get a status of over-limit and lose access to the group. You can free up space for new members by removing those who no longer need access or toggling them to over-limit. To get an unlimited number of members, you can %{link_start}upgrade%{link_end} to a paid tier."
|
||||
msgstr ""
|
||||
|
||||
msgid "Bitbucket Server Import"
|
||||
msgstr ""
|
||||
|
||||
|
@ -43524,7 +43530,7 @@ msgstr ""
|
|||
msgid "WorkItem|Collapse child items"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Convert to work item"
|
||||
msgid "WorkItem|Create task"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Create work item"
|
||||
|
|
|
@ -15,10 +15,15 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
|||
import Description from '~/issues/show/components/description.vue';
|
||||
import { updateHistory } from '~/lib/utils/url_utility';
|
||||
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
|
||||
import workItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
|
||||
import createWorkItemFromTaskMutation from '~/work_items/graphql/create_work_item_from_task.mutation.graphql';
|
||||
import TaskList from '~/task_list';
|
||||
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
|
||||
import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
|
||||
import CreateWorkItem from '~/work_items/pages/create_work_item.vue';
|
||||
import {
|
||||
projectWorkItemTypesQueryResponse,
|
||||
createWorkItemFromTaskMutationResponse,
|
||||
} from 'jest/work_items/mock_data';
|
||||
import {
|
||||
descriptionProps as initialProps,
|
||||
descriptionHtmlWithCheckboxes,
|
||||
|
@ -46,6 +51,10 @@ const workItemQueryResponse = {
|
|||
};
|
||||
|
||||
const queryHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
|
||||
const workItemTypesQueryHandler = jest.fn().mockResolvedValue(projectWorkItemTypesQueryResponse);
|
||||
const createWorkItemFromTaskSuccessHandler = jest
|
||||
.fn()
|
||||
.mockResolvedValue(createWorkItemFromTaskMutationResponse);
|
||||
|
||||
describe('Description component', () => {
|
||||
let wrapper;
|
||||
|
@ -60,18 +69,24 @@ describe('Description component', () => {
|
|||
|
||||
const findTooltips = () => wrapper.findAllComponents(GlTooltip);
|
||||
const findModal = () => wrapper.findComponent(GlModal);
|
||||
const findCreateWorkItem = () => wrapper.findComponent(CreateWorkItem);
|
||||
const findWorkItemDetailModal = () => wrapper.findComponent(WorkItemDetailModal);
|
||||
|
||||
function createComponent({ props = {}, provide = {} } = {}) {
|
||||
function createComponent({ props = {}, provide } = {}) {
|
||||
wrapper = shallowMountExtended(Description, {
|
||||
propsData: {
|
||||
issueId: 1,
|
||||
...initialProps,
|
||||
...props,
|
||||
},
|
||||
provide,
|
||||
apolloProvider: createMockApollo([[workItemQuery, queryHandler]]),
|
||||
provide: {
|
||||
fullPath: 'gitlab-org/gitlab-test',
|
||||
...provide,
|
||||
},
|
||||
apolloProvider: createMockApollo([
|
||||
[workItemQuery, queryHandler],
|
||||
[workItemTypesQuery, workItemTypesQueryHandler],
|
||||
[createWorkItemFromTaskMutation, createWorkItemFromTaskSuccessHandler],
|
||||
]),
|
||||
mocks: {
|
||||
$toast,
|
||||
},
|
||||
|
@ -299,24 +314,16 @@ describe('Description component', () => {
|
|||
});
|
||||
|
||||
it('does not show a modal by default', () => {
|
||||
expect(findModal().props('visible')).toBe(false);
|
||||
expect(findModal().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('opens a modal when a button is clicked and displays correct title', async () => {
|
||||
await findConvertToTaskButton().trigger('click');
|
||||
expect(findCreateWorkItem().props('initialTitle').trim()).toBe('todo 1');
|
||||
});
|
||||
|
||||
it('closes the modal on `closeCreateTaskModal` event', async () => {
|
||||
await findConvertToTaskButton().trigger('click');
|
||||
findCreateWorkItem().vm.$emit('closeModal');
|
||||
expect(hideModal).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('emits `updateDescription` on `onCreate` event', () => {
|
||||
it('emits `updateDescription` after creating new work item', async () => {
|
||||
const newDescription = `<p>New description</p>`;
|
||||
findCreateWorkItem().vm.$emit('onCreate', newDescription);
|
||||
expect(hideModal).toHaveBeenCalled();
|
||||
|
||||
await findConvertToTaskButton().trigger('click');
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.emitted('updateDescription')).toEqual([[newDescription]]);
|
||||
});
|
||||
|
||||
|
|
|
@ -140,13 +140,45 @@ export const createWorkItemFromTaskMutationResponse = {
|
|||
__typename: 'WorkItemCreateFromTaskPayload',
|
||||
errors: [],
|
||||
workItem: {
|
||||
descriptionHtml: '<p>New description</p>',
|
||||
id: 'gid://gitlab/WorkItem/13',
|
||||
__typename: 'WorkItem',
|
||||
description: 'New description',
|
||||
id: 'gid://gitlab/WorkItem/1',
|
||||
title: 'Updated title',
|
||||
state: 'OPEN',
|
||||
workItemType: {
|
||||
__typename: 'WorkItemType',
|
||||
id: 'gid://gitlab/WorkItems::Type/5',
|
||||
name: 'Task',
|
||||
},
|
||||
userPermissions: {
|
||||
deleteWorkItem: false,
|
||||
updateWorkItem: false,
|
||||
},
|
||||
widgets: [
|
||||
{
|
||||
__typename: 'WorkItemWidgetDescription',
|
||||
type: 'DESCRIPTION',
|
||||
description: 'New description',
|
||||
descriptionHtml: '<p>New description</p>',
|
||||
},
|
||||
],
|
||||
},
|
||||
newWorkItem: {
|
||||
__typename: 'WorkItem',
|
||||
id: 'gid://gitlab/WorkItem/1000000',
|
||||
title: 'Updated title',
|
||||
state: 'OPEN',
|
||||
description: '',
|
||||
workItemType: {
|
||||
__typename: 'WorkItemType',
|
||||
id: 'gid://gitlab/WorkItems::Type/5',
|
||||
name: 'Task',
|
||||
},
|
||||
userPermissions: {
|
||||
deleteWorkItem: false,
|
||||
updateWorkItem: false,
|
||||
},
|
||||
widgets: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -9,11 +9,7 @@ import ItemTitle from '~/work_items/components/item_title.vue';
|
|||
import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
|
||||
import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql';
|
||||
import createWorkItemFromTaskMutation from '~/work_items/graphql/create_work_item_from_task.mutation.graphql';
|
||||
import {
|
||||
projectWorkItemTypesQueryResponse,
|
||||
createWorkItemMutationResponse,
|
||||
createWorkItemFromTaskMutationResponse,
|
||||
} from '../mock_data';
|
||||
import { projectWorkItemTypesQueryResponse, createWorkItemMutationResponse } from '../mock_data';
|
||||
|
||||
jest.mock('~/lib/utils/uuids', () => ({ uuids: () => ['testuuid'] }));
|
||||
|
||||
|
@ -25,9 +21,6 @@ describe('Create work item component', () => {
|
|||
|
||||
const querySuccessHandler = jest.fn().mockResolvedValue(projectWorkItemTypesQueryResponse);
|
||||
const createWorkItemSuccessHandler = jest.fn().mockResolvedValue(createWorkItemMutationResponse);
|
||||
const createWorkItemFromTaskSuccessHandler = jest
|
||||
.fn()
|
||||
.mockResolvedValue(createWorkItemFromTaskMutationResponse);
|
||||
const errorHandler = jest.fn().mockRejectedValue('Houston, we have a problem');
|
||||
|
||||
const findAlert = () => wrapper.findComponent(GlAlert);
|
||||
|
@ -122,49 +115,6 @@ describe('Create work item component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when displayed in a modal', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
props: {
|
||||
isModal: true,
|
||||
},
|
||||
mutationHandler: createWorkItemFromTaskSuccessHandler,
|
||||
});
|
||||
});
|
||||
|
||||
it('emits `closeModal` event on Cancel button click', () => {
|
||||
findCancelButton().vm.$emit('click');
|
||||
|
||||
expect(wrapper.emitted('closeModal')).toEqual([[]]);
|
||||
});
|
||||
|
||||
it('emits `onCreate` on successful mutation', async () => {
|
||||
findTitleInput().vm.$emit('title-input', 'Test title');
|
||||
|
||||
wrapper.find('form').trigger('submit');
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.emitted('onCreate')).toEqual([['<p>New description</p>']]);
|
||||
});
|
||||
|
||||
it('does not right margin for create button', () => {
|
||||
expect(findCreateButton().classes()).not.toContain('gl-mr-3');
|
||||
});
|
||||
|
||||
it('adds right margin for cancel button', () => {
|
||||
expect(findCancelButton().classes()).toContain('gl-mr-3');
|
||||
});
|
||||
|
||||
it('adds padding for content', () => {
|
||||
expect(findContent().classes('gl-px-5')).toBe(true);
|
||||
});
|
||||
|
||||
it('defaults type to `Task`', async () => {
|
||||
await waitForPromises();
|
||||
expect(findSelect().attributes('value')).toBe('gid://gitlab/WorkItems::Type/3');
|
||||
});
|
||||
});
|
||||
|
||||
it('displays a loading icon inside dropdown when work items query is loading', () => {
|
||||
createComponent();
|
||||
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
*.gem
|
||||
.bundle
|
|
@ -0,0 +1,32 @@
|
|||
# You can override the included template(s) by including variable overrides
|
||||
# SAST customization: https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings
|
||||
# Secret Detection customization: https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings
|
||||
# Dependency Scanning customization: https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#customizing-the-dependency-scanning-settings
|
||||
# Note that environment variables can be set in several places
|
||||
# See https://docs.gitlab.com/ee/ci/variables/#cicd-variable-precedence
|
||||
workflow:
|
||||
rules:
|
||||
- if: $CI_MERGE_REQUEST_ID
|
||||
|
||||
.rspec:
|
||||
cache:
|
||||
key: ipynbdiff
|
||||
paths:
|
||||
- vendor/gems/ipynbdiff/vendor/ruby
|
||||
before_script:
|
||||
- cd vendor/gems/ipynbdiff
|
||||
- ruby -v # Print out ruby version for debugging
|
||||
- gem install bundler --no-document # Bundler is not installed with the image
|
||||
- bundle config set --local path 'vendor' # Install dependencies into ./vendor/ruby
|
||||
- bundle config set with 'development'
|
||||
- bundle install -j $(nproc)
|
||||
script:
|
||||
- bundle exec rspec
|
||||
|
||||
rspec-2.7:
|
||||
image: "ruby:2.7"
|
||||
extends: .rspec
|
||||
|
||||
rspec-3.0:
|
||||
image: "ruby:3.0"
|
||||
extends: .rspec
|
|
@ -0,0 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
source 'https://rubygems.org'
|
||||
|
||||
gemspec
|
|
@ -0,0 +1,64 @@
|
|||
PATH
|
||||
remote: .
|
||||
specs:
|
||||
ipynbdiff (0.4.7)
|
||||
diffy (~> 3.3)
|
||||
json (~> 2.5, >= 2.5.1)
|
||||
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
ast (2.4.2)
|
||||
binding_ninja (0.2.3)
|
||||
coderay (1.1.3)
|
||||
diff-lcs (1.5.0)
|
||||
diffy (3.4.2)
|
||||
json (2.6.2)
|
||||
method_source (1.0.0)
|
||||
parser (3.1.2.0)
|
||||
ast (~> 2.4.1)
|
||||
proc_to_ast (0.1.0)
|
||||
coderay
|
||||
parser
|
||||
unparser
|
||||
pry (0.14.1)
|
||||
coderay (~> 1.1)
|
||||
method_source (~> 1.0)
|
||||
rake (13.0.6)
|
||||
rspec (3.11.0)
|
||||
rspec-core (~> 3.11.0)
|
||||
rspec-expectations (~> 3.11.0)
|
||||
rspec-mocks (~> 3.11.0)
|
||||
rspec-core (3.11.0)
|
||||
rspec-support (~> 3.11.0)
|
||||
rspec-expectations (3.11.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.11.0)
|
||||
rspec-mocks (3.11.1)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.11.0)
|
||||
rspec-parameterized (0.5.1)
|
||||
binding_ninja (>= 0.2.3)
|
||||
parser
|
||||
proc_to_ast
|
||||
rspec (>= 2.13, < 4)
|
||||
unparser
|
||||
rspec-support (3.11.0)
|
||||
unparser (0.6.5)
|
||||
diff-lcs (~> 1.3)
|
||||
parser (>= 3.1.0)
|
||||
|
||||
PLATFORMS
|
||||
x86_64-darwin-20
|
||||
x86_64-linux
|
||||
|
||||
DEPENDENCIES
|
||||
bundler (~> 2.2)
|
||||
ipynbdiff!
|
||||
pry (~> 0.14)
|
||||
rake (~> 13.0)
|
||||
rspec (~> 3.10)
|
||||
rspec-parameterized (~> 0.5.1)
|
||||
|
||||
BUNDLED WITH
|
||||
2.3.16
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016-2021 GitLab B.V.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
|
@ -0,0 +1,56 @@
|
|||
# IpynbDiff: Better diff for Jupyter Notebooks
|
||||
|
||||
This is a simple diff tool that cleans up Jupyter notebooks, transforming each [notebook](example/1/from.ipynb)
|
||||
into a [readable markdown file](example/1/from_html.md), keeping the output of cells, and running the
|
||||
diff after. Markdowns are generated using an opinionated Jupyter to Markdown conversion. This means
|
||||
that the entire file is readable on the diff.
|
||||
|
||||
The result are diffs that are much easier to read:
|
||||
|
||||
| Diff | IpynbDiff |
|
||||
| ----------------------------------- | ----------------------------------------------------- |
|
||||
| [Diff text](example/diff.txt) | [IpynbDiff text](example/ipynbdiff_percent.txt) |
|
||||
| ![Diff image](example/img/diff.png) | ![IpynbDiff image](example/img/ipynbdiff_percent.png) |
|
||||
|
||||
This started as a port of [ipynbdiff](https://gitlab.com/gitlab-org/incubation-engineering/mlops/poc/ipynbdiff),
|
||||
but now has extended functionality although not working as git driver.
|
||||
|
||||
## Usage
|
||||
|
||||
### Generating diffs
|
||||
|
||||
```ruby
|
||||
IpynbDiff.diff(from_path, to_path, options)
|
||||
```
|
||||
|
||||
Options:
|
||||
|
||||
```ruby
|
||||
@default_transform_options = {
|
||||
preprocess_input: true, # Whether the input should be transformed
|
||||
write_output_to: nil, # Pass a path to save the output to a file
|
||||
format: :text, # These are the formats Diffy accepts https://github.com/samg/diffy
|
||||
sources_are_files: false, # Weather to use the from/to as string or path to a file
|
||||
raise_if_invalid_notebook: false, # Raises an error if the notebooks are invalid, otherwise returns nil
|
||||
transform_options: @default_transform_options, # See below for transform options
|
||||
diff_opts: {
|
||||
include_diff_info: false # These are passed to Diffy https://github.com/samg/diffy
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Transforming the notebooks
|
||||
|
||||
It might be necessary to have the transformed files in addition to the diff.
|
||||
|
||||
```ruby
|
||||
IpynbDiff.transform(notebook, options)
|
||||
```
|
||||
|
||||
Options:
|
||||
|
||||
```ruby
|
||||
@default_transform_options = {
|
||||
include_frontmatter: false, # Whether to include or not the notebook metadata (kernel, language, etc)
|
||||
}
|
||||
```
|
|
@ -0,0 +1,34 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
lib = File.expand_path('lib/..', __dir__)
|
||||
$LOAD_PATH.unshift lib unless $LOAD_PATH.include?(lib)
|
||||
|
||||
require 'lib/version'
|
||||
|
||||
Gem::Specification.new do |s|
|
||||
s.name = 'ipynbdiff'
|
||||
s.version = IpynbDiff::VERSION
|
||||
s.summary = 'Human Readable diffs for Jupyter Notebooks'
|
||||
s.description = 'Better diff for Jupyter Notebooks by first preprocessing them and removing clutter'
|
||||
s.authors = ['Eduardo Bonet']
|
||||
s.email = 'ebonet@gitlab.com'
|
||||
# Specify which files should be added to the gem when it is released.
|
||||
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
||||
s.files = Dir.chdir(File.expand_path(__dir__)) do
|
||||
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(spec|example)/}) }
|
||||
end
|
||||
s.homepage =
|
||||
'https://gitlab.com/gitlab-org/incubation-engineering/mlops/rb-ipynbdiff'
|
||||
s.license = 'MIT'
|
||||
|
||||
s.require_paths = ['lib']
|
||||
|
||||
s.add_runtime_dependency 'diffy', '~> 3.3'
|
||||
s.add_runtime_dependency 'json', '~> 2.5', '>= 2.5.1'
|
||||
|
||||
s.add_development_dependency 'bundler', '~> 2.2'
|
||||
s.add_development_dependency 'pry', '~> 0.14'
|
||||
s.add_development_dependency 'rake', '~> 13.0'
|
||||
s.add_development_dependency 'rspec', '~> 3.10'
|
||||
s.add_development_dependency 'rspec-parameterized', '~> 0.5.1'
|
||||
end
|
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Custom differ for Jupyter Notebooks
|
||||
module IpynbDiff
|
||||
require 'delegate'
|
||||
|
||||
# The result of a diff object
|
||||
class Diff < SimpleDelegator
|
||||
require 'diffy'
|
||||
|
||||
attr_reader :from, :to
|
||||
|
||||
def initialize(from, to, diffy_opts)
|
||||
super(Diffy::Diff.new(from.as_text, to.as_text, **diffy_opts))
|
||||
|
||||
@from = from
|
||||
@to = to
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,218 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module IpynbDiff
|
||||
class InvalidTokenError < StandardError
|
||||
end
|
||||
|
||||
# Creates a symbol map for a ipynb file (JSON format)
|
||||
class IpynbSymbolMap
|
||||
class << self
|
||||
def parse(notebook, objects_to_ignore = [])
|
||||
IpynbSymbolMap.new(notebook, objects_to_ignore).parse('')
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :current_line, :char_idx, :results
|
||||
|
||||
WHITESPACE_CHARS = ["\t", "\r", ' ', "\n"].freeze
|
||||
|
||||
VALUE_STOPPERS = [',', '[', ']', '{', '}', *WHITESPACE_CHARS].freeze
|
||||
|
||||
def initialize(notebook, objects_to_ignore = [])
|
||||
@chars = notebook.chars
|
||||
@current_line = 0
|
||||
@char_idx = 0
|
||||
@results = {}
|
||||
@objects_to_ignore = objects_to_ignore
|
||||
end
|
||||
|
||||
def parse(prefix = '.')
|
||||
raise_if_file_ended
|
||||
|
||||
skip_whitespaces
|
||||
|
||||
if (c = current_char) == '"'
|
||||
parse_string
|
||||
elsif c == '['
|
||||
parse_array(prefix)
|
||||
elsif c == '{'
|
||||
parse_object(prefix)
|
||||
else
|
||||
parse_value
|
||||
end
|
||||
|
||||
results
|
||||
end
|
||||
|
||||
def parse_array(prefix)
|
||||
# [1, 2, {"some": "object"}, [1]]
|
||||
|
||||
i = 0
|
||||
|
||||
current_should_be '['
|
||||
|
||||
loop do
|
||||
raise_if_file_ended
|
||||
|
||||
break if skip_beginning(']')
|
||||
|
||||
new_prefix = "#{prefix}.#{i}"
|
||||
|
||||
add_result(new_prefix, current_line)
|
||||
|
||||
parse(new_prefix)
|
||||
|
||||
i += 1
|
||||
end
|
||||
end
|
||||
|
||||
def parse_object(prefix)
|
||||
# {"name":"value", "another_name": [1, 2, 3]}
|
||||
|
||||
current_should_be '{'
|
||||
|
||||
loop do
|
||||
raise_if_file_ended
|
||||
|
||||
break if skip_beginning('}')
|
||||
|
||||
prop_name = parse_string(return_value: true)
|
||||
|
||||
next_and_skip_whitespaces
|
||||
|
||||
current_should_be ':'
|
||||
|
||||
next_and_skip_whitespaces
|
||||
|
||||
if @objects_to_ignore.include? prop_name
|
||||
skip
|
||||
else
|
||||
new_prefix = "#{prefix}.#{prop_name}"
|
||||
|
||||
add_result(new_prefix, current_line)
|
||||
|
||||
parse(new_prefix)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def parse_string(return_value: false)
|
||||
current_should_be '"'
|
||||
init_idx = @char_idx
|
||||
|
||||
loop do
|
||||
increment_char_index
|
||||
|
||||
raise_if_file_ended
|
||||
|
||||
if current_char == '"' && !prev_backslash?
|
||||
init_idx += 1
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
@chars[init_idx...@char_idx].join if return_value
|
||||
end
|
||||
|
||||
def add_result(key, line_number)
|
||||
@results[key] = line_number
|
||||
end
|
||||
|
||||
def parse_value
|
||||
increment_char_index until raise_if_file_ended || VALUE_STOPPERS.include?(current_char)
|
||||
end
|
||||
|
||||
def skip_whitespaces
|
||||
while WHITESPACE_CHARS.include?(current_char)
|
||||
raise_if_file_ended
|
||||
check_for_new_line
|
||||
increment_char_index
|
||||
end
|
||||
end
|
||||
|
||||
def increment_char_index
|
||||
@char_idx += 1
|
||||
end
|
||||
|
||||
def next_and_skip_whitespaces
|
||||
increment_char_index
|
||||
skip_whitespaces
|
||||
end
|
||||
|
||||
def current_char
|
||||
raise_if_file_ended
|
||||
|
||||
@chars[@char_idx]
|
||||
end
|
||||
|
||||
def prev_backslash?
|
||||
@chars[@char_idx - 1] == '\\' && @chars[@char_idx - 2] != '\\'
|
||||
end
|
||||
|
||||
def current_should_be(another_char)
|
||||
raise InvalidTokenError unless current_char == another_char
|
||||
end
|
||||
|
||||
def check_for_new_line
|
||||
@current_line += 1 if current_char == "\n"
|
||||
end
|
||||
|
||||
def raise_if_file_ended
|
||||
@char_idx >= @chars.size && raise(InvalidTokenError)
|
||||
end
|
||||
|
||||
def skip
|
||||
raise_if_file_ended
|
||||
|
||||
skip_whitespaces
|
||||
|
||||
if (c = current_char) == '"'
|
||||
parse_string
|
||||
elsif c == '['
|
||||
skip_array
|
||||
elsif c == '{'
|
||||
skip_object
|
||||
else
|
||||
parse_value
|
||||
end
|
||||
end
|
||||
|
||||
def skip_array
|
||||
loop do
|
||||
raise_if_file_ended
|
||||
|
||||
break if skip_beginning(']')
|
||||
|
||||
skip
|
||||
end
|
||||
end
|
||||
|
||||
def skip_object
|
||||
loop do
|
||||
raise_if_file_ended
|
||||
|
||||
break if skip_beginning('}')
|
||||
|
||||
parse_string
|
||||
|
||||
next_and_skip_whitespaces
|
||||
|
||||
current_should_be ':'
|
||||
|
||||
next_and_skip_whitespaces
|
||||
|
||||
skip
|
||||
end
|
||||
end
|
||||
|
||||
def skip_beginning(closing_char)
|
||||
check_for_new_line
|
||||
|
||||
next_and_skip_whitespaces
|
||||
|
||||
return true if current_char == closing_char
|
||||
|
||||
next_and_skip_whitespaces if current_char == ','
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Human Readable Jupyter Diffs
|
||||
module IpynbDiff
|
||||
require 'transformer'
|
||||
require 'diff'
|
||||
|
||||
def self.diff(from, to, raise_if_invalid_nb: false, include_frontmatter: false, hide_images: false, diffy_opts: {})
|
||||
transformer = Transformer.new(include_frontmatter: include_frontmatter, hide_images: hide_images)
|
||||
|
||||
Diff.new(transformer.transform(from), transformer.transform(to), diffy_opts)
|
||||
rescue InvalidNotebookError
|
||||
raise if raise_if_invalid_nb
|
||||
end
|
||||
|
||||
def self.transform(notebook, raise_errors: false, include_frontmatter: true, hide_images: false)
|
||||
return unless notebook
|
||||
|
||||
Transformer.new(include_frontmatter: include_frontmatter, hide_images: hide_images).transform(notebook).as_text
|
||||
rescue InvalidNotebookError
|
||||
raise if raise_errors
|
||||
end
|
||||
end
|
|
@ -0,0 +1,83 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module IpynbDiff
|
||||
# Transforms Jupyter output data into markdown
|
||||
class OutputTransformer
|
||||
require 'symbolized_markdown_helper'
|
||||
include SymbolizedMarkdownHelper
|
||||
|
||||
HIDDEN_IMAGE_OUTPUT = ' [Hidden Image Output]'
|
||||
|
||||
ORDERED_KEYS = {
|
||||
'execute_result' => %w[image/png image/svg+xml image/jpeg text/markdown text/latex text/plain],
|
||||
'display_data' => %w[image/png image/svg+xml image/jpeg text/markdown text/latex],
|
||||
'stream' => %w[text]
|
||||
}.freeze
|
||||
|
||||
def initialize(hide_images: false)
|
||||
@hide_images = hide_images
|
||||
end
|
||||
|
||||
def transform(output, symbol)
|
||||
transformed = case (output_type = output['output_type'])
|
||||
when 'error'
|
||||
transform_error(output['traceback'], symbol / 'traceback')
|
||||
when 'execute_result', 'display_data'
|
||||
transform_non_error(ORDERED_KEYS[output_type], output['data'], symbol / 'data')
|
||||
when 'stream'
|
||||
transform_element('text', output['text'], symbol)
|
||||
end
|
||||
|
||||
transformed ? decorate_output(transformed, output, symbol) : []
|
||||
end
|
||||
|
||||
def decorate_output(output_rows, output, symbol)
|
||||
[
|
||||
_,
|
||||
_(symbol, %(%%%% Output: #{output['output_type']})),
|
||||
_,
|
||||
*output_rows
|
||||
]
|
||||
end
|
||||
|
||||
def transform_error(traceback, symbol)
|
||||
traceback.map.with_index do |t, idx|
|
||||
t.split("\n").map do |l|
|
||||
_(symbol / idx, l.gsub(/\[[0-9][0-9;]*m/, '').sub("\u001B", ' ').gsub(/\u001B/, '').rstrip)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def transform_non_error(accepted_keys, elements, symbol)
|
||||
accepted_keys.filter { |key| elements.key?(key) }.map do |key|
|
||||
transform_element(key, elements[key], symbol)
|
||||
end
|
||||
end
|
||||
|
||||
def transform_element(output_type, output_element, symbol_prefix)
|
||||
new_symbol = symbol_prefix / output_type
|
||||
case output_type
|
||||
when 'image/png', 'image/jpeg'
|
||||
transform_image(output_type + ';base64', output_element, new_symbol)
|
||||
when 'image/svg+xml'
|
||||
transform_image(output_type + ';utf8', output_element, new_symbol)
|
||||
when 'text/markdown', 'text/latex', 'text/plain', 'text'
|
||||
transform_text(output_element, new_symbol)
|
||||
end
|
||||
end
|
||||
|
||||
def transform_image(image_type, image_content, symbol)
|
||||
return _(nil, HIDDEN_IMAGE_OUTPUT) if @hide_images
|
||||
|
||||
lines = image_content.is_a?(Array) ? image_content : [image_content]
|
||||
|
||||
single_line = lines.map(&:strip).join.gsub(/\s+/, ' ')
|
||||
|
||||
_(symbol, " ![](data:#{image_type},#{single_line})")
|
||||
end
|
||||
|
||||
def transform_text(text_content, symbol)
|
||||
symbolize_array(symbol, text_content) { |l| " #{l.rstrip}" }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,26 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module IpynbDiff
|
||||
# Helper functions
|
||||
module SymbolizedMarkdownHelper
|
||||
|
||||
def _(symbol = nil, content = '')
|
||||
{ symbol: symbol, content: content }
|
||||
end
|
||||
|
||||
def symbolize_array(symbol, content, &block)
|
||||
if content.is_a?(Array)
|
||||
content.map.with_index { |l, idx| _(symbol / idx, block.call(l)) }
|
||||
else
|
||||
_(symbol, content)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Simple wrapper for a string
|
||||
class JsonSymbol < String
|
||||
def /(other)
|
||||
JsonSymbol.new((other.is_a?(Array) ? [self, *other] : [self, other]).join('.'))
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module IpynbDiff
|
||||
# Notebook that was transformed into md, including location of source cells
|
||||
class TransformedNotebook
|
||||
attr_reader :blocks
|
||||
|
||||
def as_text
|
||||
@blocks.map { |b| b[:content] }.join("\n")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def initialize(lines = [], symbol_map = {})
|
||||
@blocks = lines.map do |line|
|
||||
{ content: line[:content], source_symbol: (symbol = line[:symbol]), source_line: symbol && symbol_map[symbol] }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,101 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module IpynbDiff
|
||||
class InvalidNotebookError < StandardError
|
||||
end
|
||||
|
||||
# Returns a markdown version of the Jupyter Notebook
|
||||
class Transformer
|
||||
require 'json'
|
||||
require 'yaml'
|
||||
require 'output_transformer'
|
||||
require 'symbolized_markdown_helper'
|
||||
require 'ipynb_symbol_map'
|
||||
require 'transformed_notebook'
|
||||
include SymbolizedMarkdownHelper
|
||||
|
||||
@include_frontmatter = true
|
||||
@objects_to_ignore = ['application/javascript', 'application/vnd.holoviews_load.v0+json']
|
||||
|
||||
def initialize(include_frontmatter: true, hide_images: false)
|
||||
@include_frontmatter = include_frontmatter
|
||||
@hide_images = hide_images
|
||||
@output_transformer = OutputTransformer.new(hide_images: hide_images)
|
||||
end
|
||||
|
||||
def validate_notebook(notebook)
|
||||
notebook_json = JSON.parse(notebook)
|
||||
|
||||
return notebook_json if notebook_json.key?('cells')
|
||||
|
||||
raise InvalidNotebookError
|
||||
rescue JSON::ParserError
|
||||
raise InvalidNotebookError
|
||||
end
|
||||
|
||||
def transform(notebook)
|
||||
return TransformedNotebook.new unless notebook
|
||||
|
||||
notebook_json = validate_notebook(notebook)
|
||||
transformed = transform_document(notebook_json)
|
||||
symbol_map = IpynbSymbolMap.parse(notebook)
|
||||
|
||||
TransformedNotebook.new(transformed, symbol_map)
|
||||
end
|
||||
|
||||
def transform_document(notebook)
|
||||
symbol = JsonSymbol.new('.cells')
|
||||
|
||||
transformed_blocks = notebook['cells'].map.with_index do |cell, idx|
|
||||
decorate_cell(transform_cell(cell, notebook, symbol / idx), cell, symbol / idx)
|
||||
end
|
||||
|
||||
transformed_blocks.prepend(transform_metadata(notebook)) if @include_frontmatter
|
||||
transformed_blocks.flatten
|
||||
end
|
||||
|
||||
def decorate_cell(rows, cell, symbol)
|
||||
tags = cell['metadata']&.fetch('tags', [])
|
||||
type = cell['cell_type'] || 'raw'
|
||||
|
||||
[
|
||||
_(symbol, %(%% Cell type:#{type} id:#{cell['id']} tags:#{tags&.join(',')})),
|
||||
_,
|
||||
rows,
|
||||
_
|
||||
]
|
||||
end
|
||||
|
||||
def transform_cell(cell, notebook, symbol)
|
||||
cell['cell_type'] == 'code' ? transform_code_cell(cell, notebook, symbol) : transform_text_cell(cell, symbol)
|
||||
end
|
||||
|
||||
def transform_code_cell(cell, notebook, symbol)
|
||||
[
|
||||
_(symbol / 'source', %(``` #{notebook.dig('metadata', 'kernelspec', 'language') || ''})),
|
||||
symbolize_array(symbol / 'source', cell['source'], &:rstrip),
|
||||
_(nil, '```'),
|
||||
cell['outputs'].map.with_index do |output, idx|
|
||||
@output_transformer.transform(output, symbol / ['outputs', idx])
|
||||
end
|
||||
]
|
||||
end
|
||||
|
||||
def transform_text_cell(cell, symbol)
|
||||
symbolize_array(symbol / 'source', cell['source'], &:rstrip)
|
||||
end
|
||||
|
||||
def transform_metadata(notebook_json)
|
||||
as_yaml = {
|
||||
'jupyter' => {
|
||||
'kernelspec' => notebook_json['metadata']['kernelspec'],
|
||||
'language_info' => notebook_json['metadata']['language_info'],
|
||||
'nbformat' => notebook_json['nbformat'],
|
||||
'nbformat_minor' => notebook_json['nbformat_minor']
|
||||
}
|
||||
}.to_yaml
|
||||
|
||||
as_yaml.split("\n").map { |l| _(nil, l) }.append(_(nil, '---'), _)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module IpynbDiff
|
||||
VERSION = '0.4.7'
|
||||
end
|
|
@ -0,0 +1,165 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rspec'
|
||||
require 'json'
|
||||
require 'rspec-parameterized'
|
||||
require 'ipynb_symbol_map'
|
||||
|
||||
describe IpynbDiff::IpynbSymbolMap do
|
||||
def res(*cases)
|
||||
cases&.to_h || []
|
||||
end
|
||||
|
||||
describe '#parse_string' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
let(:mapper) { IpynbDiff::IpynbSymbolMap.new(input) }
|
||||
|
||||
where(:input, :result) do
|
||||
# Empty string
|
||||
'""' | ''
|
||||
# Some string with quotes
|
||||
'"he\nll\"o"' | 'he\nll\"o'
|
||||
end
|
||||
|
||||
with_them do
|
||||
it { expect(mapper.parse_string(return_value: true)).to eq(result) }
|
||||
it { expect(mapper.parse_string).to be_nil }
|
||||
it { expect(mapper.results).to be_empty }
|
||||
end
|
||||
|
||||
it 'raises if invalid string' do
|
||||
mapper = IpynbDiff::IpynbSymbolMap.new('"')
|
||||
|
||||
expect { mapper.parse_string }.to raise_error(IpynbDiff::InvalidTokenError)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe '#parse_object' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
let(:mapper) { IpynbDiff::IpynbSymbolMap.new(notebook, objects_to_ignore) }
|
||||
|
||||
before do
|
||||
mapper.parse_object('')
|
||||
end
|
||||
|
||||
where(:notebook, :objects_to_ignore, :result) do
|
||||
# Empty object
|
||||
'{ }' | [] | res
|
||||
# Object with string
|
||||
'{ "hello" : "world" }' | [] | res(['.hello', 0])
|
||||
# Object with boolean
|
||||
'{ "hello" : true }' | [] | res(['.hello', 0])
|
||||
# Object with integer
|
||||
'{ "hello" : 1 }' | [] | res(['.hello', 0])
|
||||
# Object with 2 properties in the same line
|
||||
'{ "hello" : "world" , "my" : "bad" }' | [] | res(['.hello', 0], ['.my', 0])
|
||||
# Object with 2 properties in the different lines line
|
||||
"{ \"hello\" : \"world\" , \n \n \"my\" : \"bad\" }" | [] | res(['.hello', 0], ['.my', 2])
|
||||
# Object with 2 properties, but one is ignored
|
||||
"{ \"hello\" : \"world\" , \n \n \"my\" : \"bad\" }" | ['hello'] | res(['.my', 2])
|
||||
end
|
||||
|
||||
with_them do
|
||||
it { expect(mapper.results).to include(result) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#parse_array' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:notebook, :result) do
|
||||
# Empty Array
|
||||
'[]' | res
|
||||
# Array with string value
|
||||
'["a"]' | res(['.0', 0])
|
||||
# Array with boolean
|
||||
'[ true ]' | res(['.0', 0])
|
||||
# Array with integer
|
||||
'[ 1 ]' | res(['.0', 0])
|
||||
# Two values on the same line
|
||||
'["a", "b"]' | res(['.0', 0], ['.1', 0])
|
||||
# With line breaks'
|
||||
"[\n \"a\" \n , \n \"b\" ]" | res(['.0', 1], ['.1', 3])
|
||||
end
|
||||
|
||||
let(:mapper) { IpynbDiff::IpynbSymbolMap.new(notebook) }
|
||||
|
||||
before do
|
||||
mapper.parse_array('')
|
||||
end
|
||||
|
||||
with_them do
|
||||
it { expect(mapper.results).to match_array(result) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#skip_object' do
|
||||
subject { IpynbDiff::IpynbSymbolMap.parse(JSON.pretty_generate(source)) }
|
||||
end
|
||||
|
||||
describe '#parse' do
|
||||
|
||||
let(:objects_to_ignore) { [] }
|
||||
|
||||
subject { IpynbDiff::IpynbSymbolMap.parse(JSON.pretty_generate(source), objects_to_ignore) }
|
||||
|
||||
context 'Empty object' do
|
||||
let(:source) { {} }
|
||||
|
||||
it { is_expected.to be_empty }
|
||||
end
|
||||
|
||||
context 'Object with inner object and number' do
|
||||
let(:source) { { obj1: { obj2: 1 } } }
|
||||
|
||||
it { is_expected.to match_array(res(['.obj1', 1], ['.obj1.obj2', 2])) }
|
||||
end
|
||||
|
||||
context 'Object with inner object and number, string and array with object' do
|
||||
let(:source) { { obj1: { obj2: [123, 2, true], obj3: "hel\nlo", obj4: true, obj5: 123, obj6: 'a' } } }
|
||||
|
||||
it do
|
||||
is_expected.to match_array(
|
||||
res(['.obj1', 1],
|
||||
['.obj1.obj2', 2],
|
||||
['.obj1.obj2.0', 3],
|
||||
['.obj1.obj2.1', 4],
|
||||
['.obj1.obj2.2', 5],
|
||||
['.obj1.obj3', 7],
|
||||
['.obj1.obj4', 8],
|
||||
['.obj1.obj5', 9],
|
||||
['.obj1.obj6', 10])
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'When index is exceeded because of failure' do
|
||||
it 'raises an exception' do
|
||||
source = '{"\\a": "a\""}'
|
||||
|
||||
mapper = IpynbDiff::IpynbSymbolMap.new(source)
|
||||
|
||||
expect(mapper).to receive(:prev_backslash?).at_least(1).time.and_return(false)
|
||||
|
||||
expect { mapper.parse('') }.to raise_error(IpynbDiff::InvalidTokenError)
|
||||
end
|
||||
end
|
||||
|
||||
context 'Object with inner object and number, string and array with object' do
|
||||
let(:source) { { obj1: { obj2: [123, 2, true], obj3: "hel\nlo", obj4: true, obj5: 123, obj6: { obj7: 'a' } } } }
|
||||
let(:objects_to_ignore) { %w(obj2 obj6) }
|
||||
it do
|
||||
is_expected.to match_array(
|
||||
res(['.obj1', 1],
|
||||
['.obj1.obj3', 7],
|
||||
['.obj1.obj4', 8],
|
||||
['.obj1.obj5', 9],
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,126 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'ipynbdiff'
|
||||
require 'rspec'
|
||||
require 'rspec-parameterized'
|
||||
|
||||
BASE_PATH = File.join(File.expand_path(File.dirname(__FILE__)), 'testdata')
|
||||
|
||||
describe IpynbDiff do
|
||||
def diff_signs(diff)
|
||||
diff.to_s(:text).scan(/.*\n/).map { |l| l[0] }.join('')
|
||||
end
|
||||
|
||||
describe 'diff' do
|
||||
let(:from_path) { File.join(BASE_PATH, 'from.ipynb') }
|
||||
let(:to_path) { File.join(BASE_PATH,'to.ipynb') }
|
||||
let(:from) { File.read(from_path) }
|
||||
let(:to) { File.read(to_path) }
|
||||
let(:include_frontmatter) { false }
|
||||
let(:hide_images) { false }
|
||||
|
||||
subject { IpynbDiff.diff(from, to, include_frontmatter: include_frontmatter, hide_images: hide_images) }
|
||||
|
||||
context 'if preprocessing is active' do
|
||||
it 'html tables are stripped' do
|
||||
is_expected.to_not include('<td>')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when to is nil' do
|
||||
let(:to) { nil }
|
||||
let(:from_path) { File.join(BASE_PATH, 'only_md', 'input.ipynb') }
|
||||
|
||||
it 'all lines are removals' do
|
||||
expect(diff_signs(subject)).to eq('-----')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when to is nil' do
|
||||
let(:from) { nil }
|
||||
let(:to_path) { File.join(BASE_PATH, 'only_md', 'input.ipynb') }
|
||||
|
||||
it 'all lines are additions' do
|
||||
expect(diff_signs(subject)).to eq('+++++')
|
||||
end
|
||||
end
|
||||
|
||||
context 'When include_frontmatter is true' do
|
||||
let(:include_frontmatter) { true }
|
||||
|
||||
it 'should show changes metadata in the metadata' do
|
||||
expect(subject.to_s(:text)).to include('+ display_name: New Python 3 (ipykernel)')
|
||||
end
|
||||
end
|
||||
|
||||
context 'When hide_images is true' do
|
||||
let(:hide_images) { true }
|
||||
|
||||
it 'hides images' do
|
||||
expect(subject.to_s(:text)).to include(' [Hidden Image Output]')
|
||||
end
|
||||
end
|
||||
|
||||
context 'When include_frontmatter is false' do
|
||||
it 'should drop metadata from the diff' do
|
||||
expect(subject.to_s(:text)).to_not include('+ display_name: New Python 3 (ipykernel)')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when either notebook can not be processed' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:ctx, :from, :to) do
|
||||
'because from is invalid' | 'a' | nil
|
||||
'because from does not have the cell tag' | '{"metadata":[]}' | nil
|
||||
'because to is invalid' | nil | 'a'
|
||||
'because to does not have the cell tag' | nil | '{"metadata":[]}'
|
||||
end
|
||||
|
||||
with_them do
|
||||
it { is_expected.to be_nil }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'transform' do
|
||||
[nil, 'a', '{"metadata":[]}'].each do |invalid_nb|
|
||||
context "when json is invalid (#{invalid_nb || 'nil'})" do
|
||||
it 'is nil' do
|
||||
expect(IpynbDiff.transform(invalid_nb)).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'options' do
|
||||
let(:include_frontmatter) { false }
|
||||
let(:hide_images) { false }
|
||||
|
||||
subject do
|
||||
IpynbDiff.transform(File.read(File.join(BASE_PATH, 'from.ipynb')),
|
||||
include_frontmatter: include_frontmatter,
|
||||
hide_images: hide_images)
|
||||
end
|
||||
|
||||
context 'include_frontmatter is false' do
|
||||
it { is_expected.to_not include('display_name: Python 3 (ipykernel)') }
|
||||
end
|
||||
|
||||
context 'include_frontmatter is true' do
|
||||
let(:include_frontmatter) { true }
|
||||
|
||||
it { is_expected.to include('display_name: Python 3 (ipykernel)') }
|
||||
end
|
||||
|
||||
context 'hide_images is false' do
|
||||
it { is_expected.not_to include('[Hidden Image Output]') }
|
||||
end
|
||||
|
||||
context 'hide_images is true' do
|
||||
let(:hide_images) { true }
|
||||
|
||||
it { is_expected.to include(' [Hidden Image Output]') }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,7 @@
|
|||
%% Cell type:markdown id: tags:
|
||||
|
||||
\
|
||||
|
||||
%% Cell type:markdown id: tags:
|
||||
|
||||
a
|
7
vendor/gems/ipynbdiff/spec/testdata/backslash_as_last_char/expected_symbols.txt
vendored
Normal file
7
vendor/gems/ipynbdiff/spec/testdata/backslash_as_last_char/expected_symbols.txt
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
.cells.0
|
||||
|
||||
.cells.0.source.0
|
||||
|
||||
.cells.1
|
||||
|
||||
.cells.1.source.0
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"\\"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"a"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
%% Cell type:code id:5 tags:
|
||||
|
||||
``` python
|
||||
# A cell that has an error
|
||||
y = sin(x)
|
||||
```
|
||||
|
||||
%%%% Output: error
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
NameError Traceback (most recent call last)
|
||||
/var/folders/cq/l637k4x13gx6y9p_gfs4c_gc0000gn/T/ipykernel_72857/3962062127.py in <module>
|
||||
1 # A cell that has an error
|
||||
----> 2 y = sin(x)
|
||||
|
||||
NameError: name 'sin' is not defined
|
|
@ -0,0 +1,16 @@
|
|||
.cells.0
|
||||
|
||||
.cells.0.source
|
||||
.cells.0.source.0
|
||||
.cells.0.source.1
|
||||
|
||||
|
||||
.cells.0.outputs.0
|
||||
|
||||
.cells.0.outputs.0.traceback.0
|
||||
.cells.0.outputs.0.traceback.1
|
||||
.cells.0.outputs.0.traceback.2
|
||||
.cells.0.outputs.0.traceback.2
|
||||
.cells.0.outputs.0.traceback.2
|
||||
.cells.0.outputs.0.traceback.2
|
||||
.cells.0.outputs.0.traceback.3
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"id": "5",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"ename": "NameError",
|
||||
"evalue": "name 'sin' is not defined",
|
||||
"output_type": "error",
|
||||
"traceback": [
|
||||
"\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
|
||||
"\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)",
|
||||
"\u001b[0;32m/var/folders/cq/l637k4x13gx6y9p_gfs4c_gc0000gn/T/ipykernel_72857/3962062127.py\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0;31m# A cell that has an error\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 2\u001b[0;31m \u001b[0my\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0msin\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mx\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
|
||||
"\u001b[0;31mNameError\u001b[0m: name 'sin' is not defined"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# A cell that has an error\n",
|
||||
"y = sin(x)"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"language": "python"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,198 @@
|
|||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "0aac5da7-745c-4eda-847a-3d0d07a1bb9b",
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"source": [
|
||||
"# This is a markdown cell\n",
|
||||
"\n",
|
||||
"This paragraph has\n",
|
||||
"With\n",
|
||||
"Many\n",
|
||||
"Lines. How we will he handle MR notes?\n",
|
||||
"\n",
|
||||
"But I can add another paragraph"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "raw",
|
||||
"id": "faecea5b-de0a-49fa-9a3a-61c2add652da",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"This is a raw cell\n",
|
||||
"With\n",
|
||||
"Multiple lines"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "893ca2c0-ab75-4276-9dad-be1c40e16e8a",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import pandas as pd\n",
|
||||
"import numpy as np\n",
|
||||
"import matplotlib.pyplot as plt"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"id": "0d707fb5-226f-46d6-80bd-489ebfb8905c",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"np.random.seed(42)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"id": "35467fcf-28b1-4c7b-bb09-4cb192c35293",
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"senoid"
|
||||
]
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"[<matplotlib.lines.Line2D at 0x123e39370>]"
|
||||
]
|
||||
},
|
||||
"execution_count": 3,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"image/png": "some_invalid_base64_image_here\n",
|
||||
"text/plain": [
|
||||
"<Figure size 432x288 with 1 Axes>"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"needs_background": "light"
|
||||
},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"x = np.linspace(0, 4*np.pi,50)\n",
|
||||
"y = np.sin(x)\n",
|
||||
"\n",
|
||||
"plt.plot(x, y)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"id": "dc1178cd-c46d-4da3-9ab5-08f000699884",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"df = pd.DataFrame({\"x\": x, \"y\": y})"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"id": "6e749b4f-b409-4700-870f-f68c39462490",
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"some-table"
|
||||
]
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/html": [
|
||||
"<div>\n",
|
||||
"<style scoped>\n",
|
||||
" .dataframe tbody tr th:only-of-type {\n",
|
||||
" vertical-align: middle;\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" .dataframe tbody tr th {\n",
|
||||
" vertical-align: top;\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" .dataframe thead th {\n",
|
||||
" text-align: right;\n",
|
||||
" }\n",
|
||||
"</style>\n",
|
||||
"<table border=\"1\" class=\"dataframe\">\n",
|
||||
" <thead>\n",
|
||||
" <tr style=\"text-align: right;\">\n",
|
||||
" <th></th>\n",
|
||||
" <th>x</th>\n",
|
||||
" <th>y</th>\n",
|
||||
" </tr>\n",
|
||||
" </thead>\n",
|
||||
" <tbody>\n",
|
||||
" <tr>\n",
|
||||
" <th>0</th>\n",
|
||||
" <td>0.000000</td>\n",
|
||||
" <td>0.000000</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>1</th>\n",
|
||||
" <td>0.256457</td>\n",
|
||||
" <td>0.253655</td>\n",
|
||||
" </tr>\n",
|
||||
" </tbody>\n",
|
||||
"</table>\n",
|
||||
"</div>"
|
||||
],
|
||||
"text/plain": [
|
||||
" x y\n",
|
||||
"0 0.000000 0.000000\n",
|
||||
"1 0.256457 0.253655"
|
||||
]
|
||||
},
|
||||
"execution_count": 5,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"df[:2]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "0ddef5ef-94a3-4afd-9c70-ddee9694f512",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.9.7"
|
||||
},
|
||||
"toc-showtags": true
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
%% Cell type:code id:5 tags:senoid
|
||||
|
||||
``` python
|
||||
```
|
||||
|
||||
%%%% Output: display_data
|
||||
|
||||
[Hidden Image Output]
|
||||
|
||||
%%%% Output: display_data
|
||||
|
||||
[Hidden Image Output]
|
|
@ -0,0 +1,12 @@
|
|||
.cells.0
|
||||
|
||||
.cells.0.source
|
||||
|
||||
|
||||
.cells.0.outputs.0
|
||||
|
||||
|
||||
|
||||
.cells.0.outputs.1
|
||||
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"id": "5",
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"senoid"
|
||||
]
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"image/png": "this_is_an_invalid_hash_for_testing_purposes\n",
|
||||
"text/plain": [
|
||||
"<Figure size 432x288 with 1 Axes>"
|
||||
]
|
||||
},
|
||||
"metadata": {
|
||||
"needs_background": "light"
|
||||
},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"image/svg+xml": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\"><circle cx=\"50\" cy=\"50\" r=\"50\"/></svg>",
|
||||
"text/plain": [
|
||||
"<IPython.core.display.SVG object>"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"language": "python"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
%% Cell type:code id:5 tags:some-table
|
||||
|
||||
``` python
|
||||
df[:2]
|
||||
```
|
||||
|
||||
%%%% Output: execute_result
|
||||
|
||||
x y
|
||||
0 0.000000 0.000000
|
||||
1 0.256457 0.507309
|
|
@ -0,0 +1,11 @@
|
|||
.cells.0
|
||||
|
||||
.cells.0.source
|
||||
.cells.0.source.0
|
||||
|
||||
|
||||
.cells.0.outputs.0
|
||||
|
||||
.cells.0.outputs.0.data.text/plain.0
|
||||
.cells.0.outputs.0.data.text/plain.1
|
||||
.cells.0.outputs.0.data.text/plain.2
|
|
@ -0,0 +1,74 @@
|
|||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"id": "5",
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"some-table"
|
||||
]
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/html": [
|
||||
"<div>\n",
|
||||
"<style scoped>\n",
|
||||
" .dataframe tbody tr th:only-of-type {\n",
|
||||
" vertical-align: middle;\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" .dataframe tbody tr th {\n",
|
||||
" vertical-align: top;\n",
|
||||
" }\n",
|
||||
"\n",
|
||||
" .dataframe thead th {\n",
|
||||
" text-align: right;\n",
|
||||
" }\n",
|
||||
"</style>\n",
|
||||
"<table border=\"1\" class=\"dataframe\">\n",
|
||||
" <thead>\n",
|
||||
" <tr style=\"text-align: right;\">\n",
|
||||
" <th></th>\n",
|
||||
" <th>x</th>\n",
|
||||
" <th>y</th>\n",
|
||||
" </tr>\n",
|
||||
" </thead>\n",
|
||||
" <tbody>\n",
|
||||
" <tr>\n",
|
||||
" <th>0</th>\n",
|
||||
" <td>0.000000</td>\n",
|
||||
" <td>0.000000</td>\n",
|
||||
" </tr>\n",
|
||||
" <tr>\n",
|
||||
" <th>1</th>\n",
|
||||
" <td>0.256457</td>\n",
|
||||
" <td>0.507309</td>\n",
|
||||
" </tr>\n",
|
||||
" </tbody>\n",
|
||||
"</table>\n",
|
||||
"</div>"
|
||||
],
|
||||
"text/plain": [
|
||||
" x y\n",
|
||||
"0 0.000000 0.000000\n",
|
||||
"1 0.256457 0.507309"
|
||||
]
|
||||
},
|
||||
"execution_count": 5,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"df[:2]"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"language": "python"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
%% Cell type:code id:5 tags:
|
||||
|
||||
``` python
|
||||
from IPython.display import display, Math
|
||||
display(Math(r'Dims: {}x{}m \\ Area: {}m^2 \\ Volume: {}m^3'.format(1, round(2,2), 3, 4)))
|
||||
```
|
||||
|
||||
%%%% Output: display_data
|
||||
|
||||
$\displaystyle Dims: 1x2m \\ Area: 3m^2 \\ Volume: 4m^3$
|
|
@ -0,0 +1,10 @@
|
|||
.cells.0
|
||||
|
||||
.cells.0.source
|
||||
.cells.0.source.0
|
||||
.cells.0.source.1
|
||||
|
||||
|
||||
.cells.0.outputs.0
|
||||
|
||||
.cells.0.outputs.0.data.text/latex.0
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"id": "5",
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/latex": [
|
||||
"$\\displaystyle Dims: 1x2m \\\\ Area: 3m^2 \\\\ Volume: 4m^3$"
|
||||
],
|
||||
"text/plain": [
|
||||
"<IPython.core.display.Math object>"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"from IPython.display import display, Math\n",
|
||||
"display(Math(r'Dims: {}x{}m \\\\ Area: {}m^2 \\\\ Volume: {}m^3'.format(1, round(2,2), 3, 4)))"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
%% Cell type:code id:5 tags:
|
||||
|
||||
```
|
||||
Some Image
|
||||
```
|
||||
|
||||
%%%% Output: display_data
|
||||
|
||||
![](data:image/png;base64,this_is_an_invalid_hash_for_testing_purposes)
|
9
vendor/gems/ipynbdiff/spec/testdata/multiline_png_output/expected_symbols.txt
vendored
Normal file
9
vendor/gems/ipynbdiff/spec/testdata/multiline_png_output/expected_symbols.txt
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
.cells.0
|
||||
|
||||
.cells.0.source
|
||||
.cells.0.source.0
|
||||
|
||||
|
||||
.cells.0.outputs.0
|
||||
|
||||
.cells.0.outputs.0.data.image/png
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"id": "5",
|
||||
"metadata": {
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"image/png": [
|
||||
"this_is_an_invalid_hash_for_testing_purposes"
|
||||
]
|
||||
},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"Some Image"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
jupyter:
|
||||
kernelspec:
|
||||
display_name: Python 3 (ipykernel)
|
||||
language: python
|
||||
name: python3
|
||||
language_info:
|
||||
codemirror_mode:
|
||||
name: ipython
|
||||
version: 3
|
||||
file_extension: ".py"
|
||||
mimetype: text/x-python
|
||||
name: python
|
||||
nbconvert_exporter: python
|
||||
pygments_lexer: ipython3
|
||||
version: 3.9.7
|
||||
nbformat: 4
|
||||
nbformat_minor: 5
|
||||
---
|
|
@ -0,0 +1,19 @@
|
|||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"cells": [],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.9.7"
|
||||
},
|
||||
"toc-showtags": true
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
0
vendor/gems/ipynbdiff/spec/testdata/no_cells_no_metadata/expected_symbols.txt
vendored
Normal file
0
vendor/gems/ipynbdiff/spec/testdata/no_cells_no_metadata/expected_symbols.txt
vendored
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"cells": [],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.9.7"
|
||||
},
|
||||
"toc-showtags": true
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
%% Cell type:markdown id:1 tags:
|
||||
|
||||
# A
|
||||
|
||||
B
|
||||
|
||||
%% Cell type:code id:3 tags:
|
||||
|
||||
``` python
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
```
|
13
vendor/gems/ipynbdiff/spec/testdata/no_metadata_on_cell/expected_symbols.txt
vendored
Normal file
13
vendor/gems/ipynbdiff/spec/testdata/no_metadata_on_cell/expected_symbols.txt
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
.cells.0
|
||||
|
||||
.cells.0.source.0
|
||||
.cells.0.source.1
|
||||
.cells.0.source.2
|
||||
|
||||
.cells.1
|
||||
|
||||
.cells.1.source
|
||||
.cells.1.source.0
|
||||
.cells.1.source.1
|
||||
.cells.1.source.2
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "1",
|
||||
"source": [
|
||||
"# A\n",
|
||||
"\n",
|
||||
"B"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "3",
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import pandas as pd\n",
|
||||
"import numpy as np\n",
|
||||
"import matplotlib.pyplot as plt"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"language": "python"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
%% Cell type:code id:3 tags:
|
||||
|
||||
``` python
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
```
|
|
@ -0,0 +1,7 @@
|
|||
.cells.0
|
||||
|
||||
.cells.0.source
|
||||
.cells.0.source.0
|
||||
.cells.0.source.1
|
||||
.cells.0.source.2
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "3",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import pandas as pd\n",
|
||||
"import numpy as np\n",
|
||||
"import matplotlib.pyplot as plt"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"language": "python"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
%% Cell type:code id:3 tags:
|
||||
|
||||
```
|
||||
|
||||
```
|
5
vendor/gems/ipynbdiff/spec/testdata/only_code_no_kernelspec/expected_symbols.txt
vendored
Normal file
5
vendor/gems/ipynbdiff/spec/testdata/only_code_no_kernelspec/expected_symbols.txt
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
.cells.0
|
||||
|
||||
.cells.0.source
|
||||
.cells.0.source
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "3",
|
||||
"source": "",
|
||||
"outputs": []
|
||||
}
|
||||
],
|
||||
"metadata": {}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
%% Cell type:code id:3 tags:
|
||||
|
||||
```
|
||||
|
||||
```
|
5
vendor/gems/ipynbdiff/spec/testdata/only_code_no_language/expected_symbols.txt
vendored
Normal file
5
vendor/gems/ipynbdiff/spec/testdata/only_code_no_language/expected_symbols.txt
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
.cells.0
|
||||
|
||||
.cells.0.source
|
||||
.cells.0.source
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "3",
|
||||
"source": "",
|
||||
"outputs": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
%% Cell type:code id:3 tags:
|
||||
|
||||
```
|
||||
|
||||
```
|
5
vendor/gems/ipynbdiff/spec/testdata/only_code_no_metadata/expected_symbols.txt
vendored
Normal file
5
vendor/gems/ipynbdiff/spec/testdata/only_code_no_metadata/expected_symbols.txt
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
.cells.0
|
||||
|
||||
.cells.0.source
|
||||
.cells.0.source
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "3",
|
||||
"source": "",
|
||||
"outputs": []
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
%% Cell type:markdown id:1 tags:hello,world
|
||||
|
||||
# A
|
||||
|
||||
B
|
|
@ -0,0 +1,5 @@
|
|||
.cells.0
|
||||
|
||||
.cells.0.source.0
|
||||
.cells.0.source.1
|
||||
.cells.0.source.2
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "1",
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"hello",
|
||||
"world"
|
||||
]
|
||||
},
|
||||
"source": [
|
||||
"# A\n",
|
||||
"\n",
|
||||
"B"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
%% Cell type:raw id:2 tags:
|
||||
|
||||
A
|
||||
B
|
|
@ -0,0 +1,4 @@
|
|||
.cells.0
|
||||
|
||||
.cells.0.source.0
|
||||
.cells.0.source.1
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "raw",
|
||||
"id": "2",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"A\n",
|
||||
"B"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
%% Cell type:markdown id:0aac5da7-745c-4eda-847a-3d0d07a1bb9b tags:
|
||||
|
||||
# This is a markdown cell
|
||||
|
||||
This paragraph has
|
||||
With
|
||||
Many
|
||||
Lines. How we will he handle MR notes?
|
||||
|
||||
But I can add another paragraph
|
||||
|
||||
%% Cell type:raw id:faecea5b-de0a-49fa-9a3a-61c2add652da tags:
|
||||
|
||||
This is a raw cell
|
||||
With
|
||||
Multiple lines
|
||||
|
||||
%% Cell type:code id:893ca2c0-ab75-4276-9dad-be1c40e16e8a tags:
|
||||
|
||||
``` python
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
```
|
||||
|
||||
%% Cell type:code id:0d707fb5-226f-46d6-80bd-489ebfb8905c tags:
|
||||
|
||||
``` python
|
||||
np.random.seed(42)
|
||||
```
|
||||
|
||||
%% Cell type:code id:35467fcf-28b1-4c7b-bb09-4cb192c35293 tags:senoid
|
||||
|
||||
``` python
|
||||
x = np.linspace(0, 4*np.pi,50)
|
||||
y = np.sin(x)
|
||||
|
||||
plt.plot(x, y)
|
||||
```
|
||||
|
||||
%%%% Output: execute_result
|
||||
|
||||
[<matplotlib.lines.Line2D at 0x123e39370>]
|
||||
|
||||
%%%% Output: display_data
|
||||
|
||||
![](data:image/png;base64,some_invalid_base64_image_here)
|
||||
|
||||
%% Cell type:code id:dc1178cd-c46d-4da3-9ab5-08f000699884 tags:
|
||||
|
||||
``` python
|
||||
df = pd.DataFrame({"x": x, "y": y})
|
||||
```
|
||||
|
||||
%% Cell type:code id:6e749b4f-b409-4700-870f-f68c39462490 tags:some-table
|
||||
|
||||
``` python
|
||||
df[:2]
|
||||
```
|
||||
|
||||
%%%% Output: execute_result
|
||||
|
||||
x y
|
||||
0 0.000000 0.000000
|
||||
1 0.256457 0.253655
|
||||
|
||||
%% Cell type:code id:0ddef5ef-94a3-4afd-9c70-ddee9694f512 tags:
|
||||
|
||||
``` python
|
||||
```
|
|
@ -0,0 +1,70 @@
|
|||
.cells.0
|
||||
|
||||
.cells.0.source.0
|
||||
.cells.0.source.1
|
||||
.cells.0.source.2
|
||||
.cells.0.source.3
|
||||
.cells.0.source.4
|
||||
.cells.0.source.5
|
||||
.cells.0.source.6
|
||||
.cells.0.source.7
|
||||
|
||||
.cells.1
|
||||
|
||||
.cells.1.source.0
|
||||
.cells.1.source.1
|
||||
.cells.1.source.2
|
||||
|
||||
.cells.2
|
||||
|
||||
.cells.2.source
|
||||
.cells.2.source.0
|
||||
.cells.2.source.1
|
||||
.cells.2.source.2
|
||||
|
||||
|
||||
.cells.3
|
||||
|
||||
.cells.3.source
|
||||
.cells.3.source.0
|
||||
|
||||
|
||||
.cells.4
|
||||
|
||||
.cells.4.source
|
||||
.cells.4.source.0
|
||||
.cells.4.source.1
|
||||
.cells.4.source.2
|
||||
.cells.4.source.3
|
||||
|
||||
|
||||
.cells.4.outputs.0
|
||||
|
||||
.cells.4.outputs.0.data.text/plain.0
|
||||
|
||||
.cells.4.outputs.1
|
||||
|
||||
.cells.4.outputs.1.data.image/png
|
||||
|
||||
.cells.5
|
||||
|
||||
.cells.5.source
|
||||
.cells.5.source.0
|
||||
|
||||
|
||||
.cells.6
|
||||
|
||||
.cells.6.source
|
||||
.cells.6.source.0
|
||||
|
||||
|
||||
.cells.6.outputs.0
|
||||
|
||||
.cells.6.outputs.0.data.text/plain.0
|
||||
.cells.6.outputs.0.data.text/plain.1
|
||||
.cells.6.outputs.0.data.text/plain.2
|
||||
|
||||
.cells.7
|
||||
|
||||
.cells.7.source
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
%% Cell type:markdown id:1 tags:hello,world
|
||||
|
||||
A
|
|
@ -0,0 +1,3 @@
|
|||
.cells.0
|
||||
|
||||
.cells.0.source
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "1",
|
||||
"metadata": {
|
||||
"tags": [
|
||||
"hello",
|
||||
"world"
|
||||
]
|
||||
},
|
||||
"source": "A"
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
%% Cell type:code id:123 tags:
|
||||
|
||||
``` python
|
||||
print("G'bye")
|
||||
```
|
||||
|
||||
%%%% Output: stream
|
||||
|
||||
G'bye
|
|
@ -0,0 +1,9 @@
|
|||
.cells.0
|
||||
|
||||
.cells.0.source
|
||||
.cells.0.source.0
|
||||
|
||||
|
||||
.cells.0.outputs.0
|
||||
|
||||
.cells.0.outputs.0.text.0
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "123",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"G'bye\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"print(\"G'bye\")"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"language": "python"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
%% Cell type:code id:5 tags:
|
||||
|
||||
``` python
|
||||
from IPython.display import SVG, display
|
||||
|
||||
svg = """<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="50" cy="50" r="50"/>
|
||||
</svg>"""
|
||||
|
||||
display(SVG(svg))
|
||||
```
|
||||
|
||||
%%%% Output: display_data
|
||||
|
||||
![](data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="50"/></svg>)
|
||||
|
||||
%%%% Output: display_data
|
||||
|
||||
![](data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="50"/></svg>)
|
|
@ -0,0 +1,19 @@
|
|||
.cells.0
|
||||
|
||||
.cells.0.source
|
||||
.cells.0.source.0
|
||||
.cells.0.source.1
|
||||
.cells.0.source.2
|
||||
.cells.0.source.3
|
||||
.cells.0.source.4
|
||||
.cells.0.source.5
|
||||
.cells.0.source.6
|
||||
|
||||
|
||||
.cells.0.outputs.0
|
||||
|
||||
.cells.0.outputs.0.data.image/svg+xml
|
||||
|
||||
.cells.0.outputs.1
|
||||
|
||||
.cells.0.outputs.1.data.image/svg+xml
|
|
@ -0,0 +1,66 @@
|
|||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
"id": "5",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"image/svg+xml": [
|
||||
"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\">\n",
|
||||
" <circle cx=\"50\" cy=\"50\" r=\"50\"/>\n",
|
||||
"</svg>"
|
||||
],
|
||||
"text/plain": [
|
||||
"<IPython.core.display.SVG object>"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"image/svg+xml": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\"><circle cx=\"50\" cy=\"50\" r=\"50\"/></svg>",
|
||||
"text/plain": [
|
||||
"<IPython.core.display.SVG object>"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"from IPython.display import SVG, display\n",
|
||||
"\n",
|
||||
"svg = \"\"\"<svg viewBox=\"0 0 100 100\" xmlns=\"http://www.w3.org/2000/svg\">\n",
|
||||
" <circle cx=\"50\" cy=\"50\" r=\"50\"/>\n",
|
||||
"</svg>\"\"\"\n",
|
||||
"\n",
|
||||
"display(SVG(svg))"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.8.12"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
%% Cell type:code id:5 tags:senoid
|
||||
|
||||
``` python
|
||||
plt.plot(x, y)
|
||||
```
|
||||
|
||||
%%%% Output: execute_result
|
||||
|
||||
[<matplotlib.lines.Line2D at 0x12a4e43d0>]
|
|
@ -0,0 +1,9 @@
|
|||
.cells.0
|
||||
|
||||
.cells.0.source
|
||||
.cells.0.source.0
|
||||
|
||||
|
||||
.cells.0.outputs.0
|
||||
|
||||
.cells.0.outputs.0.data.text/plain.0
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue