Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-06-29 09:09:41 +00:00
parent 06bd645177
commit 3fdeaff80e
109 changed files with 2871 additions and 208 deletions

View File

@ -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 #
##################

View File

@ -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

View File

@ -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'

View File

@ -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)

View File

@ -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>

View File

@ -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

View File

@ -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"
>

View File

@ -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"

View File

@ -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>

View File

@ -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>

View File

@ -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">

View File

@ -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>

View File

@ -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';

View File

@ -1,8 +1,12 @@
#import "./work_item.fragment.graphql"
mutation workItemCreateFromTask($input: WorkItemCreateFromTaskInput!) {
workItemCreateFromTask(input: $input) {
workItem {
id
descriptionHtml
...WorkItem
}
newWorkItem {
...WorkItem
}
errors
}

View File

@ -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') }}

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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. |

View File

@ -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

View File

@ -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"

View File

@ -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]]);
});

View File

@ -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: [],
},
},
},

View File

@ -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();

2
vendor/gems/ipynbdiff/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*.gem
.bundle

32
vendor/gems/ipynbdiff/.gitlab-ci.yml vendored Normal file
View File

@ -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

5
vendor/gems/ipynbdiff/Gemfile vendored Normal file
View File

@ -0,0 +1,5 @@
# frozen_string_literal: true
source 'https://rubygems.org'
gemspec

64
vendor/gems/ipynbdiff/Gemfile.lock vendored Normal file
View File

@ -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

21
vendor/gems/ipynbdiff/LICENSE vendored Normal file
View File

@ -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.

56
vendor/gems/ipynbdiff/README.md vendored Normal file
View File

@ -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)
}
```

34
vendor/gems/ipynbdiff/ipynbdiff.gemspec vendored Normal file
View File

@ -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

20
vendor/gems/ipynbdiff/lib/diff.rb vendored Normal file
View File

@ -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

View File

@ -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

23
vendor/gems/ipynbdiff/lib/ipynbdiff.rb vendored Normal file
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

101
vendor/gems/ipynbdiff/lib/transformer.rb vendored Normal file
View File

@ -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

5
vendor/gems/ipynbdiff/lib/version.rb vendored Normal file
View File

@ -0,0 +1,5 @@
# frozen_string_literal: true
module IpynbDiff
VERSION = '0.4.7'
end

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,7 @@
%% Cell type:markdown id: tags:
\
%% Cell type:markdown id: tags:
a

View File

@ -0,0 +1,7 @@
.cells.0
.cells.0.source.0
.cells.1
.cells.1.source.0

View File

@ -0,0 +1,16 @@
{
"cells": [
{
"cell_type": "markdown",
"source": [
"\\"
]
},
{
"cell_type": "markdown",
"source": [
"a"
]
}
]
}

View File

@ -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

View File

@ -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

View File

@ -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"
}
}
}

View File

@ -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
}

View File

@ -0,0 +1,12 @@
%% Cell type:code id:5 tags:senoid
``` python
```
%%%% Output: display_data
[Hidden Image Output]
%%%% Output: display_data
[Hidden Image Output]

View File

@ -0,0 +1,12 @@
.cells.0
.cells.0.source
.cells.0.outputs.0
.cells.0.outputs.1

View File

@ -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"
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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"
}
}
}

View File

@ -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$

View File

@ -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

View File

@ -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"
}
}
}

View File

@ -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)

View 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

View File

@ -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": {
}
}

View File

@ -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
---

View File

@ -0,0 +1,19 @@

View 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
}

View 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
}

View File

@ -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
```

View 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

View File

@ -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"
}
}
}

View File

@ -0,0 +1,7 @@
%% Cell type:code id:3 tags:
``` python
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
```

View File

@ -0,0 +1,7 @@
.cells.0
.cells.0.source
.cells.0.source.0
.cells.0.source.1
.cells.0.source.2

View File

@ -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"
}
}
}

View File

@ -0,0 +1,5 @@
%% Cell type:code id:3 tags:
```
```

View File

@ -0,0 +1,5 @@
.cells.0
.cells.0.source
.cells.0.source

View File

@ -0,0 +1,12 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"id": "3",
"source": "",
"outputs": []
}
],
"metadata": {}
}

View File

@ -0,0 +1,5 @@
%% Cell type:code id:3 tags:
```
```

View File

@ -0,0 +1,5 @@
.cells.0
.cells.0.source
.cells.0.source

View File

@ -0,0 +1,14 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"id": "3",
"source": "",
"outputs": []
}
],
"metadata": {
"kernelspec": {}
}
}

View File

@ -0,0 +1,5 @@
%% Cell type:code id:3 tags:
```
```

View File

@ -0,0 +1,5 @@
.cells.0
.cells.0.source
.cells.0.source

View File

@ -0,0 +1,11 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"id": "3",
"source": "",
"outputs": []
}
]
}

View File

@ -0,0 +1,5 @@
%% Cell type:markdown id:1 tags:hello,world
# A
B

View File

@ -0,0 +1,5 @@
.cells.0
.cells.0.source.0
.cells.0.source.1
.cells.0.source.2

View File

@ -0,0 +1,21 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "1",
"metadata": {
"tags": [
"hello",
"world"
]
},
"source": [
"# A\n",
"\n",
"B"
]
}
],
"metadata": {
}
}

View File

@ -0,0 +1,4 @@
%% Cell type:raw id:2 tags:
A
B

View File

@ -0,0 +1,4 @@
.cells.0
.cells.0.source.0
.cells.0.source.1

View File

@ -0,0 +1,15 @@
{
"cells": [
{
"cell_type": "raw",
"id": "2",
"metadata": {},
"source": [
"A\n",
"B"
]
}
],
"metadata": {
}
}

View File

@ -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
```

View File

@ -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

View File

@ -0,0 +1,3 @@
%% Cell type:markdown id:1 tags:hello,world
A

View File

@ -0,0 +1,3 @@
.cells.0
.cells.0.source

View File

@ -0,0 +1,17 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "1",
"metadata": {
"tags": [
"hello",
"world"
]
},
"source": "A"
}
],
"metadata": {
}
}

View File

@ -0,0 +1,9 @@
%% Cell type:code id:123 tags:
``` python
print("G'bye")
```
%%%% Output: stream
G'bye

View File

@ -0,0 +1,9 @@
.cells.0
.cells.0.source
.cells.0.source.0
.cells.0.outputs.0
.cells.0.outputs.0.text.0

View File

@ -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"
}
}
}

View File

@ -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>)

View File

@ -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

View File

@ -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
}

View File

@ -0,0 +1,9 @@
%% Cell type:code id:5 tags:senoid
``` python
plt.plot(x, y)
```
%%%% Output: execute_result
[<matplotlib.lines.Line2D at 0x12a4e43d0>]

View File

@ -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