feat: redo notebook tutorials (#1922)
<!-- .github/pull_request_template.md -->
## Description
<!--
Please provide a clear, human-generated description of the changes in
this PR.
DO NOT use AI-generated descriptions. We want to understand your thought
process and reasoning.
-->
## Acceptance Criteria
<!--
* Key requirements to the new feature or modification;
* Proof that the changes work and meet the requirements;
* Include instructions on how to verify the changes. Describe how to
test it locally;
* Proof that it's sufficiently tested.
-->
## Type of Change
<!-- Please check the relevant option -->
- [ ] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Code refactoring
- [ ] Performance improvement
- [ ] Other (please specify):
## Screenshots/Videos (if applicable)
<!-- Add screenshots or videos to help explain your changes -->
## Pre-submission Checklist
<!-- Please check all boxes that apply before submitting your PR -->
- [ ] **I have tested my changes thoroughly before submitting this PR**
- [ ] **This PR contains minimal changes necessary to address the
issue/feature**
- [ ] My code follows the project's coding standards and style
guidelines
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] I have added necessary documentation (if applicable)
- [ ] All new and existing tests pass
- [ ] I have searched existing PRs to ensure this change hasn't been
submitted already
- [ ] I have linked any relevant issues in the description
- [ ] My commits have clear and descriptive messages
## DCO Affirmation
I affirm that all code in every commit of this pull request conforms to
the terms of the Topoteretes Developer Certificate of Origin.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Two interactive tutorial notebooks added (Cognee Basics, Python
Development) with runnable code and rich markdown; MarkdownPreview for
rendered markdown; instance-aware notebook support and cloud proxy with
API key handling; notebook CRUD (create, save, run, delete).
* **Bug Fixes**
* Improved authentication handling to treat 401/403 consistently.
* **Improvements**
* Auto-expanding text areas; better error propagation from dataset
operations; migration to allow toggling deletability for legacy tutorial
notebooks.
* **Tests**
* Expanded tests for tutorial creation and loading.
<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
commit
a0f25f4f50
57 changed files with 4152 additions and 762 deletions
|
|
@ -0,0 +1,52 @@
|
|||
"""Enable delete for old tutorial notebooks
|
||||
|
||||
Revision ID: 1a58b986e6e1
|
||||
Revises: 46a6ce2bd2b2
|
||||
Create Date: 2025-12-17 11:04:44.414259
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "1a58b986e6e1"
|
||||
down_revision: Union[str, None] = "46a6ce2bd2b2"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def change_tutorial_deletable_flag(deletable: bool) -> None:
|
||||
bind = op.get_bind()
|
||||
inspector = sa.inspect(bind)
|
||||
|
||||
if "notebooks" not in inspector.get_table_names():
|
||||
return
|
||||
|
||||
columns = {col["name"] for col in inspector.get_columns("notebooks")}
|
||||
required_columns = {"name", "deletable"}
|
||||
if not required_columns.issubset(columns):
|
||||
return
|
||||
|
||||
notebooks = sa.table(
|
||||
"notebooks",
|
||||
sa.Column("name", sa.String()),
|
||||
sa.Column("deletable", sa.Boolean()),
|
||||
)
|
||||
|
||||
tutorial_name = "Python Development with Cognee Tutorial 🧠"
|
||||
|
||||
bind.execute(
|
||||
notebooks.update().where(notebooks.c.name == tutorial_name).values(deletable=deletable)
|
||||
)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
change_tutorial_deletable_flag(True)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
change_tutorial_deletable_flag(False)
|
||||
1286
cognee-frontend/package-lock.json
generated
1286
cognee-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -9,14 +9,15 @@
|
|||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@auth0/nextjs-auth0": "^4.13.1",
|
||||
"@auth0/nextjs-auth0": "^4.14.0",
|
||||
"classnames": "^2.5.1",
|
||||
"culori": "^4.0.1",
|
||||
"d3-force-3d": "^3.0.6",
|
||||
"next": "16.0.4",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"next": "^16.1.0",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"react-force-graph-2d": "^1.27.1",
|
||||
"react-markdown": "^10.1.0",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -33,4 +34,4 @@
|
|||
"tailwindcss": "^4.1.7",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,8 @@ import AddDataToCognee from "./AddDataToCognee";
|
|||
import NotebooksAccordion from "./NotebooksAccordion";
|
||||
import CogneeInstancesAccordion from "./CogneeInstancesAccordion";
|
||||
import InstanceDatasetsAccordion from "./InstanceDatasetsAccordion";
|
||||
import cloudFetch from "@/modules/instances/cloudFetch";
|
||||
import localFetch from "@/modules/instances/localFetch";
|
||||
|
||||
interface DashboardProps {
|
||||
user?: {
|
||||
|
|
@ -26,6 +28,17 @@ interface DashboardProps {
|
|||
accessToken: string;
|
||||
}
|
||||
|
||||
const cogneeInstances = {
|
||||
cloudCognee: {
|
||||
name: "CloudCognee",
|
||||
fetch: cloudFetch,
|
||||
},
|
||||
localCognee: {
|
||||
name: "LocalCognee",
|
||||
fetch: localFetch,
|
||||
}
|
||||
};
|
||||
|
||||
export default function Dashboard({ accessToken }: DashboardProps) {
|
||||
fetch.setAccessToken(accessToken);
|
||||
const { user } = useAuthenticatedUser();
|
||||
|
|
@ -38,7 +51,7 @@ export default function Dashboard({ accessToken }: DashboardProps) {
|
|||
updateNotebook,
|
||||
saveNotebook,
|
||||
removeNotebook,
|
||||
} = useNotebooks();
|
||||
} = useNotebooks(cogneeInstances.localCognee);
|
||||
|
||||
useEffect(() => {
|
||||
if (!notebooks.length) {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useCallback, useEffect } from "react";
|
|||
|
||||
import { fetch, isCloudEnvironment, useBoolean } from "@/utils";
|
||||
import { checkCloudConnection } from "@/modules/cloud";
|
||||
import { setApiKey } from "@/modules/instances/cloudFetch";
|
||||
import { CaretIcon, CloseIcon, CloudIcon, LocalCogneeIcon } from "@/ui/Icons";
|
||||
import { CTAButton, GhostButton, IconButton, Input, Modal } from "@/ui/elements";
|
||||
|
||||
|
|
@ -24,6 +25,7 @@ export default function InstanceDatasetsAccordion({ onDatasetsChange }: Instance
|
|||
const checkConnectionToCloudCognee = useCallback((apiKey?: string) => {
|
||||
if (apiKey) {
|
||||
fetch.setApiKey(apiKey);
|
||||
setApiKey(apiKey);
|
||||
}
|
||||
return checkCloudConnection()
|
||||
.then(setCloudCogneeConnected)
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ function useDatasets(useCloud = false) {
|
|||
})
|
||||
.catch((error) => {
|
||||
console.error('Error fetching datasets:', error);
|
||||
throw error;
|
||||
});
|
||||
}, [useCloud]);
|
||||
|
||||
|
|
|
|||
59
cognee-frontend/src/modules/instances/cloudFetch.ts
Normal file
59
cognee-frontend/src/modules/instances/cloudFetch.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import handleServerErrors from "@/utils/handleServerErrors";
|
||||
|
||||
// let numberOfRetries = 0;
|
||||
|
||||
const cloudApiUrl = process.env.NEXT_PUBLIC_CLOUD_API_URL || "http://localhost:8001";
|
||||
|
||||
let apiKey: string | null = process.env.NEXT_PUBLIC_COGWIT_API_KEY || null;
|
||||
|
||||
export function setApiKey(newApiKey: string) {
|
||||
apiKey = newApiKey;
|
||||
};
|
||||
|
||||
export default async function cloudFetch(url: URL | RequestInfo, options: RequestInit = {}): Promise<Response> {
|
||||
// function retry(lastError: Response) {
|
||||
// if (numberOfRetries >= 1) {
|
||||
// return Promise.reject(lastError);
|
||||
// }
|
||||
|
||||
// numberOfRetries += 1;
|
||||
|
||||
// return global.fetch("/auth/token")
|
||||
// .then(() => {
|
||||
// return fetch(url, options);
|
||||
// });
|
||||
// }
|
||||
|
||||
const authHeaders = {
|
||||
"Authorization": `X-Api-Key ${apiKey}`,
|
||||
};
|
||||
|
||||
return global.fetch(
|
||||
cloudApiUrl + "/api" + (typeof url === "string" ? url : url.toString()).replace("/v1", ""),
|
||||
{
|
||||
...options,
|
||||
headers: {
|
||||
...options.headers,
|
||||
...authHeaders,
|
||||
} as HeadersInit,
|
||||
credentials: "include",
|
||||
},
|
||||
)
|
||||
.then((response) => handleServerErrors(response, null, true))
|
||||
.catch((error) => {
|
||||
if (error.message === "NEXT_REDIRECT") {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (error.detail === undefined) {
|
||||
return Promise.reject(
|
||||
new Error("No connection to the server.")
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
});
|
||||
// .finally(() => {
|
||||
// numberOfRetries = 0;
|
||||
// });
|
||||
}
|
||||
27
cognee-frontend/src/modules/instances/localFetch.ts
Normal file
27
cognee-frontend/src/modules/instances/localFetch.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import handleServerErrors from "@/utils/handleServerErrors";
|
||||
|
||||
const localApiUrl = process.env.NEXT_PUBLIC_LOCAL_API_URL || "http://localhost:8000";
|
||||
|
||||
export default async function localFetch(url: URL | RequestInfo, options: RequestInit = {}): Promise<Response> {
|
||||
return global.fetch(
|
||||
localApiUrl + "/api" + (typeof url === "string" ? url : url.toString()),
|
||||
{
|
||||
...options,
|
||||
credentials: "include",
|
||||
},
|
||||
)
|
||||
.then((response) => handleServerErrors(response, null, false))
|
||||
.catch((error) => {
|
||||
if (error.message === "NEXT_REDIRECT") {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (error.detail === undefined) {
|
||||
return Promise.reject(
|
||||
new Error("No connection to the server.")
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
});
|
||||
}
|
||||
4
cognee-frontend/src/modules/instances/types.ts
Normal file
4
cognee-frontend/src/modules/instances/types.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export interface CogneeInstance {
|
||||
name: string;
|
||||
fetch: typeof global.fetch;
|
||||
}
|
||||
13
cognee-frontend/src/modules/notebooks/createNotebook.ts
Normal file
13
cognee-frontend/src/modules/notebooks/createNotebook.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { CogneeInstance } from "@/modules/instances/types";
|
||||
|
||||
export default function createNotebook(notebookName: string, instance: CogneeInstance) {
|
||||
return instance.fetch("/v1/notebooks/", {
|
||||
body: JSON.stringify({ name: notebookName }),
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}).then((response: Response) =>
|
||||
response.ok ? response.json() : Promise.reject(response)
|
||||
);
|
||||
}
|
||||
7
cognee-frontend/src/modules/notebooks/deleteNotebook.ts
Normal file
7
cognee-frontend/src/modules/notebooks/deleteNotebook.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { CogneeInstance } from "@/modules/instances/types";
|
||||
|
||||
export default function deleteNotebook(notebookId: string, instance: CogneeInstance) {
|
||||
return instance.fetch(`/v1/notebooks/${notebookId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
12
cognee-frontend/src/modules/notebooks/getNotebooks.ts
Normal file
12
cognee-frontend/src/modules/notebooks/getNotebooks.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { CogneeInstance } from "@/modules/instances/types";
|
||||
|
||||
export default function getNotebooks(instance: CogneeInstance) {
|
||||
return instance.fetch("/v1/notebooks/", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}).then((response: Response) =>
|
||||
response.ok ? response.json() : Promise.reject(response)
|
||||
);
|
||||
}
|
||||
14
cognee-frontend/src/modules/notebooks/runNotebookCell.ts
Normal file
14
cognee-frontend/src/modules/notebooks/runNotebookCell.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { Cell } from "@/ui/elements/Notebook/types";
|
||||
import { CogneeInstance } from "@/modules/instances/types";
|
||||
|
||||
export default function runNotebookCell(notebookId: string, cell: Cell, instance: CogneeInstance) {
|
||||
return instance.fetch(`/v1/notebooks/${notebookId}/${cell.id}/run`, {
|
||||
body: JSON.stringify({
|
||||
content: cell.content,
|
||||
}),
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}).then((response: Response) => response.json());
|
||||
}
|
||||
13
cognee-frontend/src/modules/notebooks/saveNotebook.ts
Normal file
13
cognee-frontend/src/modules/notebooks/saveNotebook.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { CogneeInstance } from "@/modules/instances/types";
|
||||
|
||||
export default function saveNotebook(notebookId: string, notebookData: object, instance: CogneeInstance) {
|
||||
return instance.fetch(`/v1/notebooks/${notebookId}`, {
|
||||
body: JSON.stringify(notebookData),
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}).then((response: Response) =>
|
||||
response.ok ? response.json() : Promise.reject(response)
|
||||
);
|
||||
}
|
||||
|
|
@ -1,20 +1,18 @@
|
|||
import { useCallback, useState } from "react";
|
||||
import { fetch, isCloudEnvironment } from "@/utils";
|
||||
import { Cell, Notebook } from "@/ui/elements/Notebook/types";
|
||||
import { CogneeInstance } from "@/modules/instances/types";
|
||||
import createNotebook from "./createNotebook";
|
||||
import deleteNotebook from "./deleteNotebook";
|
||||
import getNotebooks from "./getNotebooks";
|
||||
import runNotebookCell from "./runNotebookCell";
|
||||
import { default as persistNotebook } from "./saveNotebook";
|
||||
|
||||
function useNotebooks() {
|
||||
function useNotebooks(instance: CogneeInstance) {
|
||||
const [notebooks, setNotebooks] = useState<Notebook[]>([]);
|
||||
|
||||
const addNotebook = useCallback((notebookName: string) => {
|
||||
return fetch("/v1/notebooks", {
|
||||
body: JSON.stringify({ name: notebookName }),
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}, isCloudEnvironment())
|
||||
.then((response) => response.json())
|
||||
.then((notebook) => {
|
||||
return createNotebook(notebookName, instance)
|
||||
.then((notebook: Notebook) => {
|
||||
setNotebooks((notebooks) => [
|
||||
...notebooks,
|
||||
notebook,
|
||||
|
|
@ -22,36 +20,29 @@ function useNotebooks() {
|
|||
|
||||
return notebook;
|
||||
});
|
||||
}, []);
|
||||
}, [instance]);
|
||||
|
||||
const removeNotebook = useCallback((notebookId: string) => {
|
||||
return fetch(`/v1/notebooks/${notebookId}`, {
|
||||
method: "DELETE",
|
||||
}, isCloudEnvironment())
|
||||
return deleteNotebook(notebookId, instance)
|
||||
.then(() => {
|
||||
setNotebooks((notebooks) =>
|
||||
notebooks.filter((notebook) => notebook.id !== notebookId)
|
||||
);
|
||||
});
|
||||
}, []);
|
||||
}, [instance]);
|
||||
|
||||
const fetchNotebooks = useCallback(() => {
|
||||
return fetch("/v1/notebooks", {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}, isCloudEnvironment())
|
||||
.then((response) => response.json())
|
||||
return getNotebooks(instance)
|
||||
.then((notebooks) => {
|
||||
setNotebooks(notebooks);
|
||||
|
||||
return notebooks;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error fetching notebooks:", error);
|
||||
console.error("Error fetching notebooks:", error.detail);
|
||||
throw error
|
||||
});
|
||||
}, []);
|
||||
}, [instance]);
|
||||
|
||||
const updateNotebook = useCallback((updatedNotebook: Notebook) => {
|
||||
setNotebooks((existingNotebooks) =>
|
||||
|
|
@ -64,20 +55,13 @@ function useNotebooks() {
|
|||
}, []);
|
||||
|
||||
const saveNotebook = useCallback((notebook: Notebook) => {
|
||||
return fetch(`/v1/notebooks/${notebook.id}`, {
|
||||
body: JSON.stringify({
|
||||
name: notebook.name,
|
||||
cells: notebook.cells,
|
||||
}),
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}, isCloudEnvironment())
|
||||
.then((response) => response.json())
|
||||
}, []);
|
||||
return persistNotebook(notebook.id, {
|
||||
name: notebook.name,
|
||||
cells: notebook.cells,
|
||||
}, instance);
|
||||
}, [instance]);
|
||||
|
||||
const runCell = useCallback((notebook: Notebook, cell: Cell, cogneeInstance: string) => {
|
||||
const runCell = useCallback((notebook: Notebook, cell: Cell) => {
|
||||
setNotebooks((existingNotebooks) =>
|
||||
existingNotebooks.map((existingNotebook) =>
|
||||
existingNotebook.id === notebook.id ? {
|
||||
|
|
@ -89,20 +73,11 @@ function useNotebooks() {
|
|||
error: undefined,
|
||||
} : existingCell
|
||||
),
|
||||
} : notebook
|
||||
} : existingNotebook
|
||||
)
|
||||
);
|
||||
|
||||
return fetch(`/v1/notebooks/${notebook.id}/${cell.id}/run`, {
|
||||
body: JSON.stringify({
|
||||
content: cell.content,
|
||||
}),
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}, cogneeInstance === "cloud")
|
||||
.then((response) => response.json())
|
||||
return runNotebookCell(notebook.id, cell, instance)
|
||||
.then((response) => {
|
||||
setNotebooks((existingNotebooks) =>
|
||||
existingNotebooks.map((existingNotebook) =>
|
||||
|
|
@ -115,11 +90,11 @@ function useNotebooks() {
|
|||
error: response.error,
|
||||
} : existingCell
|
||||
),
|
||||
} : notebook
|
||||
} : existingNotebook
|
||||
)
|
||||
);
|
||||
});
|
||||
}, []);
|
||||
}, [instance]);
|
||||
|
||||
return {
|
||||
notebooks,
|
||||
|
|
|
|||
77
cognee-frontend/src/ui/elements/Notebook/MarkdownPreview.tsx
Normal file
77
cognee-frontend/src/ui/elements/Notebook/MarkdownPreview.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import { memo } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
|
||||
interface MarkdownPreviewProps {
|
||||
content: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function MarkdownPreview({ content, className = "" }: MarkdownPreviewProps) {
|
||||
return (
|
||||
<div className={`min-h-24 max-h-96 overflow-y-auto p-4 prose prose-sm max-w-none ${className}`}>
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
h1: ({ children }) => <h1 className="text-2xl font-bold mt-4 mb-2">{children}</h1>,
|
||||
h2: ({ children }) => <h2 className="text-xl font-bold mt-3 mb-2">{children}</h2>,
|
||||
h3: ({ children }) => <h3 className="text-lg font-bold mt-3 mb-2">{children}</h3>,
|
||||
h4: ({ children }) => <h4 className="text-base font-bold mt-2 mb-1">{children}</h4>,
|
||||
h5: ({ children }) => <h5 className="text-sm font-bold mt-2 mb-1">{children}</h5>,
|
||||
h6: ({ children }) => <h6 className="text-xs font-bold mt-2 mb-1">{children}</h6>,
|
||||
p: ({ children }) => <p className="mb-2">{children}</p>,
|
||||
ul: ({ children }) => <ul className="list-disc list-inside mb-2 ml-4">{children}</ul>,
|
||||
ol: ({ children }) => <ol className="list-decimal list-inside mb-2 ml-4">{children}</ol>,
|
||||
li: ({ children }) => <li className="mb-1">{children}</li>,
|
||||
blockquote: ({ children }) => (
|
||||
<blockquote className="border-l-4 border-gray-300 pl-4 italic my-2">{children}</blockquote>
|
||||
),
|
||||
code: ({ className, children, ...props }) => {
|
||||
const isInline = !className;
|
||||
return isInline ? (
|
||||
<code className="bg-gray-100 px-1 py-0.5 rounded text-sm font-mono" {...props}>
|
||||
{children}
|
||||
</code>
|
||||
) : (
|
||||
<code className="block bg-gray-100 p-2 rounded text-sm font-mono overflow-x-auto" {...props}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
pre: ({ children }) => (
|
||||
<pre className="bg-gray-100 p-2 rounded text-sm font-mono overflow-x-auto mb-2">
|
||||
{children}
|
||||
</pre>
|
||||
),
|
||||
a: ({ href, children }) => (
|
||||
<a href={href} className="text-blue-600 hover:underline" target="_blank" rel="noopener noreferrer">
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
strong: ({ children }) => <strong className="font-bold">{children}</strong>,
|
||||
em: ({ children }) => <em className="italic">{children}</em>,
|
||||
hr: () => <hr className="my-4 border-gray-300" />,
|
||||
table: ({ children }) => (
|
||||
<div className="overflow-x-auto my-2">
|
||||
<table className="min-w-full border border-gray-300">{children}</table>
|
||||
</div>
|
||||
),
|
||||
thead: ({ children }) => <thead className="bg-gray-100">{children}</thead>,
|
||||
tbody: ({ children }) => <tbody>{children}</tbody>,
|
||||
tr: ({ children }) => <tr className="border-b border-gray-300">{children}</tr>,
|
||||
th: ({ children }) => (
|
||||
<th className="border border-gray-300 px-4 py-2 text-left font-bold">
|
||||
{children}
|
||||
</th>
|
||||
),
|
||||
td: ({ children }) => (
|
||||
<td className="border border-gray-300 px-4 py-2">{children}</td>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(MarkdownPreview);
|
||||
|
||||
|
|
@ -2,15 +2,17 @@
|
|||
|
||||
import { v4 as uuid4 } from "uuid";
|
||||
import classNames from "classnames";
|
||||
import { Fragment, MouseEvent, RefObject, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Fragment, MouseEvent, MutableRefObject, useCallback, useEffect, useRef, useState, memo } from "react";
|
||||
|
||||
import { useModal } from "@/ui/elements/Modal";
|
||||
import { CaretIcon, CloseIcon, PlusIcon } from "@/ui/Icons";
|
||||
import { IconButton, PopupMenu, TextArea, Modal, GhostButton, CTAButton } from "@/ui/elements";
|
||||
import PopupMenu from "@/ui/elements/PopupMenu";
|
||||
import { IconButton, TextArea, Modal, GhostButton, CTAButton } from "@/ui/elements";
|
||||
import { GraphControlsAPI } from "@/app/(graph)/GraphControls";
|
||||
import GraphVisualization, { GraphVisualizationAPI } from "@/app/(graph)/GraphVisualization";
|
||||
|
||||
import NotebookCellHeader from "./NotebookCellHeader";
|
||||
import MarkdownPreview from "./MarkdownPreview";
|
||||
import { Cell, Notebook as NotebookType } from "./types";
|
||||
|
||||
interface NotebookProps {
|
||||
|
|
@ -19,7 +21,186 @@ interface NotebookProps {
|
|||
updateNotebook: (updatedNotebook: NotebookType) => void;
|
||||
}
|
||||
|
||||
interface NotebookCellProps {
|
||||
cell: Cell;
|
||||
index: number;
|
||||
isOpen: boolean;
|
||||
isMarkdownEditMode: boolean;
|
||||
onToggleOpen: () => void;
|
||||
onToggleMarkdownEdit: () => void;
|
||||
onContentChange: (value: string) => void;
|
||||
onCellRun: (cell: Cell, cogneeInstance: string) => Promise<void>;
|
||||
onCellRename: (cell: Cell) => void;
|
||||
onCellRemove: (cell: Cell) => void;
|
||||
onCellUp: (cell: Cell) => void;
|
||||
onCellDown: (cell: Cell) => void;
|
||||
onCellAdd: (afterCellIndex: number, cellType: "markdown" | "code") => void;
|
||||
}
|
||||
|
||||
const NotebookCell = memo(function NotebookCell({
|
||||
cell,
|
||||
index,
|
||||
isOpen,
|
||||
isMarkdownEditMode,
|
||||
onToggleOpen,
|
||||
onToggleMarkdownEdit,
|
||||
onContentChange,
|
||||
onCellRun,
|
||||
onCellRename,
|
||||
onCellRemove,
|
||||
onCellUp,
|
||||
onCellDown,
|
||||
onCellAdd,
|
||||
}: NotebookCellProps) {
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="flex flex-row rounded-xl border-1 border-gray-100">
|
||||
<div className="flex flex-col flex-1 relative">
|
||||
{cell.type === "code" ? (
|
||||
<>
|
||||
<div className="absolute left-[-1.35rem] top-2.5">
|
||||
<IconButton className="p-[0.25rem] m-[-0.25rem]" onClick={onToggleOpen}>
|
||||
<CaretIcon className={classNames("transition-transform", isOpen ? "rotate-0" : "rotate-180")} />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<NotebookCellHeader
|
||||
cell={cell}
|
||||
runCell={onCellRun}
|
||||
renameCell={onCellRename}
|
||||
removeCell={onCellRemove}
|
||||
moveCellUp={onCellUp}
|
||||
moveCellDown={onCellDown}
|
||||
className="rounded-tl-xl rounded-tr-xl"
|
||||
/>
|
||||
|
||||
{isOpen && (
|
||||
<>
|
||||
<TextArea
|
||||
value={cell.content}
|
||||
onChange={onContentChange}
|
||||
isAutoExpanding
|
||||
name="cellInput"
|
||||
placeholder="Type your code here..."
|
||||
className="resize-none min-h-36 max-h-96 overflow-y-auto rounded-tl-none rounded-tr-none rounded-bl-xl rounded-br-xl border-0 !outline-0"
|
||||
/>
|
||||
|
||||
<div className="flex flex-col bg-gray-100 overflow-x-auto max-w-full">
|
||||
{cell.result && (
|
||||
<div className="px-2 py-2">
|
||||
output: <CellResult content={cell.result} />
|
||||
</div>
|
||||
)}
|
||||
{!!cell.error?.length && (
|
||||
<div className="px-2 py-2">
|
||||
error: {cell.error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="absolute left-[-1.35rem] top-2.5">
|
||||
<IconButton className="p-[0.25rem] m-[-0.25rem]" onClick={onToggleOpen}>
|
||||
<CaretIcon className={classNames("transition-transform", isOpen ? "rotate-0" : "rotate-180")} />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<NotebookCellHeader
|
||||
cell={cell}
|
||||
renameCell={onCellRename}
|
||||
removeCell={onCellRemove}
|
||||
moveCellUp={onCellUp}
|
||||
moveCellDown={onCellDown}
|
||||
className="rounded-tl-xl rounded-tr-xl"
|
||||
/>
|
||||
|
||||
{isOpen && (
|
||||
<div className="relative rounded-tl-none rounded-tr-none rounded-bl-xl rounded-br-xl border-0 overflow-hidden">
|
||||
<GhostButton
|
||||
onClick={onToggleMarkdownEdit}
|
||||
className="absolute top-2 right-2.5 text-xs leading-[1] !px-2 !py-1 !h-auto"
|
||||
>
|
||||
{isMarkdownEditMode ? "Preview" : "Edit"}
|
||||
</GhostButton>
|
||||
{isMarkdownEditMode ? (
|
||||
<TextArea
|
||||
value={cell.content}
|
||||
onChange={onContentChange}
|
||||
isAutoExpanding
|
||||
name="markdownInput"
|
||||
placeholder="Type your markdown here..."
|
||||
className="resize-none min-h-24 max-h-96 overflow-y-auto rounded-tl-none rounded-tr-none rounded-bl-xl rounded-br-xl border-0 !outline-0 !bg-gray-50"
|
||||
/>
|
||||
) : (
|
||||
<MarkdownPreview content={cell.content} className="!bg-gray-50" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-[-1.35rem]">
|
||||
<PopupMenu
|
||||
openToRight={true}
|
||||
triggerElement={<PlusIcon />}
|
||||
triggerClassName="p-[0.25rem] m-[-0.25rem]"
|
||||
>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<button
|
||||
onClick={() => onCellAdd(index, "markdown")}
|
||||
className="hover:bg-gray-100 w-full text-left px-2 cursor-pointer"
|
||||
>
|
||||
<span>text</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => onCellAdd(index, "code")}
|
||||
className="hover:bg-gray-100 w-full text-left px-2 cursor-pointer"
|
||||
>
|
||||
<span>code</span>
|
||||
</div>
|
||||
</PopupMenu>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
});
|
||||
|
||||
export default function Notebook({ notebook, updateNotebook, runCell }: NotebookProps) {
|
||||
const [openCells, setOpenCells] = useState(new Set(notebook.cells.map((c: Cell) => c.id)));
|
||||
const [markdownEditMode, setMarkdownEditMode] = useState<Set<string>>(new Set());
|
||||
|
||||
const toggleCellOpen = useCallback((id: string) => {
|
||||
setOpenCells((prev) => {
|
||||
const newState = new Set(prev);
|
||||
|
||||
if (newState.has(id)) {
|
||||
newState.delete(id)
|
||||
} else {
|
||||
newState.add(id);
|
||||
}
|
||||
|
||||
return newState;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const toggleMarkdownEditMode = useCallback((id: string) => {
|
||||
setMarkdownEditMode((prev) => {
|
||||
const newState = new Set(prev);
|
||||
|
||||
if (newState.has(id)) {
|
||||
newState.delete(id);
|
||||
} else {
|
||||
newState.add(id);
|
||||
}
|
||||
|
||||
return newState;
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (notebook.cells.length === 0) {
|
||||
const newCell: Cell = {
|
||||
|
|
@ -34,7 +215,7 @@ export default function Notebook({ notebook, updateNotebook, runCell }: Notebook
|
|||
});
|
||||
toggleCellOpen(newCell.id)
|
||||
}
|
||||
}, [notebook, updateNotebook]);
|
||||
}, [notebook, updateNotebook, toggleCellOpen]);
|
||||
|
||||
const handleCellRun = useCallback((cell: Cell, cogneeInstance: string) => {
|
||||
return runCell(notebook, cell, cogneeInstance);
|
||||
|
|
@ -43,7 +224,7 @@ export default function Notebook({ notebook, updateNotebook, runCell }: Notebook
|
|||
const handleCellAdd = useCallback((afterCellIndex: number, cellType: "markdown" | "code") => {
|
||||
const newCell: Cell = {
|
||||
id: uuid4(),
|
||||
name: "new cell",
|
||||
name: cellType === "markdown" ? "Markdown Cell" : "Code Cell",
|
||||
type: cellType,
|
||||
content: "",
|
||||
};
|
||||
|
|
@ -59,7 +240,7 @@ export default function Notebook({ notebook, updateNotebook, runCell }: Notebook
|
|||
|
||||
toggleCellOpen(newCell.id);
|
||||
updateNotebook(newNotebook);
|
||||
}, [notebook, updateNotebook]);
|
||||
}, [notebook, updateNotebook, toggleCellOpen]);
|
||||
|
||||
const removeCell = useCallback((cell: Cell, event?: MouseEvent) => {
|
||||
event?.preventDefault();
|
||||
|
|
@ -81,14 +262,12 @@ export default function Notebook({ notebook, updateNotebook, runCell }: Notebook
|
|||
openCellRemoveConfirmModal(cell);
|
||||
}, [openCellRemoveConfirmModal]);
|
||||
|
||||
const handleCellInputChange = useCallback((notebook: NotebookType, cell: Cell, value: string) => {
|
||||
const newCell = {...cell, content: value };
|
||||
|
||||
const handleCellInputChange = useCallback((cellId: string, value: string) => {
|
||||
updateNotebook({
|
||||
...notebook,
|
||||
cells: notebook.cells.map((cell: Cell) => (cell.id === newCell.id ? newCell : cell)),
|
||||
cells: notebook.cells.map((cell: Cell) => (cell.id === cellId ? {...cell, content: value} : cell)),
|
||||
});
|
||||
}, [updateNotebook]);
|
||||
}, [notebook, updateNotebook]);
|
||||
|
||||
const handleCellUp = useCallback((cell: Cell) => {
|
||||
const index = notebook.cells.indexOf(cell);
|
||||
|
|
@ -131,133 +310,28 @@ export default function Notebook({ notebook, updateNotebook, runCell }: Notebook
|
|||
}
|
||||
}, [notebook, updateNotebook]);
|
||||
|
||||
const [openCells, setOpenCells] = useState(new Set(notebook.cells.map((c: Cell) => c.id)));
|
||||
|
||||
const toggleCellOpen = (id: string) => {
|
||||
setOpenCells((prev) => {
|
||||
const newState = new Set(prev);
|
||||
|
||||
if (newState.has(id)) {
|
||||
newState.delete(id)
|
||||
} else {
|
||||
newState.add(id);
|
||||
}
|
||||
|
||||
return newState;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-white rounded-xl flex flex-col gap-0.5 px-7 py-5 flex-1">
|
||||
<div className="mb-5">{notebook.name}</div>
|
||||
|
||||
{notebook.cells.map((cell: Cell, index) => (
|
||||
<Fragment key={cell.id}>
|
||||
<div key={cell.id} className="flex flex-row rounded-xl border-1 border-gray-100">
|
||||
<div className="flex flex-col flex-1 relative">
|
||||
{cell.type === "code" ? (
|
||||
<>
|
||||
<div className="absolute left-[-1.35rem] top-2.5">
|
||||
<IconButton className="p-[0.25rem] m-[-0.25rem]" onClick={toggleCellOpen.bind(null, cell.id)}>
|
||||
<CaretIcon className={classNames("transition-transform", openCells.has(cell.id) ? "rotate-0" : "rotate-180")} />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<NotebookCellHeader
|
||||
cell={cell}
|
||||
runCell={handleCellRun}
|
||||
renameCell={handleCellRename}
|
||||
removeCell={handleCellRemove}
|
||||
moveCellUp={handleCellUp}
|
||||
moveCellDown={handleCellDown}
|
||||
className="rounded-tl-xl rounded-tr-xl"
|
||||
/>
|
||||
|
||||
{openCells.has(cell.id) && (
|
||||
<>
|
||||
<TextArea
|
||||
value={cell.content}
|
||||
onChange={handleCellInputChange.bind(null, notebook, cell)}
|
||||
// onKeyUp={handleCellRunOnEnter}
|
||||
isAutoExpanding
|
||||
name="cellInput"
|
||||
placeholder="Type your code here..."
|
||||
contentEditable={true}
|
||||
className="resize-none min-h-36 max-h-96 overflow-y-auto rounded-tl-none rounded-tr-none rounded-bl-xl rounded-br-xl border-0 !outline-0"
|
||||
/>
|
||||
|
||||
<div className="flex flex-col bg-gray-100 overflow-x-auto max-w-full">
|
||||
{cell.result && (
|
||||
<div className="px-2 py-2">
|
||||
output: <CellResult content={cell.result} />
|
||||
</div>
|
||||
)}
|
||||
{!!cell.error?.length && (
|
||||
<div className="px-2 py-2">
|
||||
error: {cell.error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="absolute left-[-1.35rem] top-2.5">
|
||||
<IconButton className="p-[0.25rem] m-[-0.25rem]" onClick={toggleCellOpen.bind(null, cell.id)}>
|
||||
<CaretIcon className={classNames("transition-transform", openCells.has(cell.id) ? "rotate-0" : "rotate-180")} />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<NotebookCellHeader
|
||||
cell={cell}
|
||||
renameCell={handleCellRename}
|
||||
removeCell={handleCellRemove}
|
||||
moveCellUp={handleCellUp}
|
||||
moveCellDown={handleCellDown}
|
||||
className="rounded-tl-xl rounded-tr-xl"
|
||||
/>
|
||||
|
||||
{openCells.has(cell.id) && (
|
||||
<TextArea
|
||||
value={cell.content}
|
||||
onChange={handleCellInputChange.bind(null, notebook, cell)}
|
||||
// onKeyUp={handleCellRunOnEnter}
|
||||
isAutoExpanding
|
||||
name="cellInput"
|
||||
placeholder="Type your text here..."
|
||||
contentEditable={true}
|
||||
className="resize-none min-h-24 max-h-96 overflow-y-auto rounded-tl-none rounded-tr-none rounded-bl-xl rounded-br-xl border-0 !outline-0"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-[-1.35rem]">
|
||||
<PopupMenu
|
||||
openToRight={true}
|
||||
triggerElement={<PlusIcon />}
|
||||
triggerClassName="p-[0.25rem] m-[-0.25rem]"
|
||||
>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<button
|
||||
onClick={() => handleCellAdd(index, "markdown")}
|
||||
className="hover:bg-gray-100 w-full text-left px-2 cursor-pointer"
|
||||
>
|
||||
<span>text</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => handleCellAdd(index, "code")}
|
||||
className="hover:bg-gray-100 w-full text-left px-2 cursor-pointer"
|
||||
>
|
||||
<span>code</span>
|
||||
</div>
|
||||
</PopupMenu>
|
||||
</div>
|
||||
</Fragment>
|
||||
<NotebookCell
|
||||
key={cell.id}
|
||||
cell={cell}
|
||||
index={index}
|
||||
isOpen={openCells.has(cell.id)}
|
||||
isMarkdownEditMode={markdownEditMode.has(cell.id)}
|
||||
onToggleOpen={() => toggleCellOpen(cell.id)}
|
||||
onToggleMarkdownEdit={() => toggleMarkdownEditMode(cell.id)}
|
||||
onContentChange={(value) => handleCellInputChange(cell.id, value)}
|
||||
onCellRun={handleCellRun}
|
||||
onCellRename={handleCellRename}
|
||||
onCellRemove={handleCellRemove}
|
||||
onCellUp={handleCellUp}
|
||||
onCellDown={handleCellDown}
|
||||
onCellAdd={handleCellAdd}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
|
@ -288,6 +362,10 @@ function CellResult({ content }: { content: [] }) {
|
|||
getSelectedNode: () => null,
|
||||
});
|
||||
|
||||
if (content.length === 0) {
|
||||
return <span>OK</span>;
|
||||
}
|
||||
|
||||
for (const line of content) {
|
||||
try {
|
||||
if (Array.isArray(line)) {
|
||||
|
|
@ -298,7 +376,7 @@ function CellResult({ content }: { content: [] }) {
|
|||
<span className="text-sm pl-2 mb-4">reasoning graph</span>
|
||||
<GraphVisualization
|
||||
data={transformInsightsGraphData(line)}
|
||||
ref={graphRef as RefObject<GraphVisualizationAPI>}
|
||||
ref={graphRef as MutableRefObject<GraphVisualizationAPI>}
|
||||
graphControls={graphControls}
|
||||
className="min-h-80"
|
||||
/>
|
||||
|
|
@ -346,7 +424,7 @@ function CellResult({ content }: { content: [] }) {
|
|||
<span className="text-sm pl-2 mb-4">reasoning graph (datasets: {datasetName})</span>
|
||||
<GraphVisualization
|
||||
data={transformToVisualizationData(graph)}
|
||||
ref={graphRef as RefObject<GraphVisualizationAPI>}
|
||||
ref={graphRef as MutableRefObject<GraphVisualizationAPI>}
|
||||
graphControls={graphControls}
|
||||
className="min-h-80"
|
||||
/>
|
||||
|
|
@ -356,8 +434,7 @@ function CellResult({ content }: { content: [] }) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof(line) === "object" && line["result"] && typeof(line["result"]) === "string") {
|
||||
else if (typeof(line) === "object" && line["result"] && typeof(line["result"]) === "string") {
|
||||
const datasets = Array.from(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
new Set(Object.values(line["datasets"]).map((dataset: any) => dataset.name))
|
||||
|
|
@ -369,39 +446,46 @@ function CellResult({ content }: { content: [] }) {
|
|||
<span className="block px-2 py-2 whitespace-normal">{line["result"]}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (typeof(line) === "object" && line["graphs"]) {
|
||||
Object.entries<{ nodes: []; edges: []; }>(line["graphs"]).forEach(([datasetName, graph]) => {
|
||||
parsedContent.push(
|
||||
<div key={datasetName} className="w-full h-full bg-white">
|
||||
<span className="text-sm pl-2 mb-4">reasoning graph (datasets: {datasetName})</span>
|
||||
<GraphVisualization
|
||||
data={transformToVisualizationData(graph)}
|
||||
ref={graphRef as RefObject<GraphVisualizationAPI>}
|
||||
graphControls={graphControls}
|
||||
className="min-h-80"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof(line) === "object" && line["result"] && typeof(line["result"]) === "object") {
|
||||
if (line["graphs"]) {
|
||||
Object.entries<{ nodes: []; edges: []; }>(line["graphs"]).forEach(([datasetName, graph]) => {
|
||||
parsedContent.push(
|
||||
<div key={datasetName} className="w-full h-full bg-white">
|
||||
<span className="text-sm pl-2 mb-4">reasoning graph (datasets: {datasetName})</span>
|
||||
<GraphVisualization
|
||||
data={transformToVisualizationData(graph)}
|
||||
ref={graphRef as MutableRefObject<GraphVisualizationAPI>}
|
||||
graphControls={graphControls}
|
||||
className="min-h-80"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (typeof(line) === "object" && line["result"] && typeof(line["result"]) === "object") {
|
||||
parsedContent.push(
|
||||
<pre className="px-2 w-full h-full bg-white text-sm" key={String(line).slice(0, -10)}>
|
||||
{JSON.stringify(line["result"], null, 2)}
|
||||
</pre>
|
||||
)
|
||||
}
|
||||
if (typeof(line) === "string") {
|
||||
else if (typeof(line) === "object") {
|
||||
parsedContent.push(
|
||||
<pre className="px-2 w-full h-full bg-white text-sm" key={String(line).slice(0, -10)}>
|
||||
{JSON.stringify(line, null, 2)}
|
||||
</pre>
|
||||
)
|
||||
}
|
||||
else if (typeof(line) === "string") {
|
||||
parsedContent.push(
|
||||
<pre className="px-2 w-full h-full bg-white text-sm whitespace-normal" key={String(line).slice(0, -10)}>
|
||||
{line}
|
||||
</pre>
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} catch {
|
||||
// It is fine if we don't manage to parse the output line, we show it as it is.
|
||||
parsedContent.push(
|
||||
<pre className="px-2 w-full h-full bg-white text-sm whitespace-normal" key={String(line).slice(0, -10)}>
|
||||
{line}
|
||||
|
|
@ -415,7 +499,6 @@ function CellResult({ content }: { content: [] }) {
|
|||
{item}
|
||||
</div>
|
||||
));
|
||||
|
||||
};
|
||||
|
||||
function transformToVisualizationData(graph: { nodes: [], edges: [] }) {
|
||||
|
|
@ -471,7 +554,7 @@ function transformInsightsGraphData(triplets: Triplet[]) {
|
|||
label: triplet[1]["relationship_name"],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
nodes: Object.values(nodes),
|
||||
links: Object.values(links),
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { isCloudEnvironment, useBoolean } from "@/utils";
|
||||
import { PlayIcon } from "@/ui/Icons";
|
||||
import { PopupMenu, IconButton } from "@/ui/elements";
|
||||
import PopupMenu from "@/ui/elements/PopupMenu";
|
||||
import { IconButton } from "@/ui/elements";
|
||||
import { LoadingIndicator } from "@/ui/App";
|
||||
|
||||
import { Cell } from "./types";
|
||||
|
|
@ -39,7 +42,7 @@ export default function NotebookCellHeader({
|
|||
if (runCell) {
|
||||
setIsRunningCell();
|
||||
runCell(cell, runInstance)
|
||||
.then(() => {
|
||||
.finally(() => {
|
||||
setIsNotRunningCell();
|
||||
});
|
||||
}
|
||||
|
|
@ -53,7 +56,7 @@ export default function NotebookCellHeader({
|
|||
{isRunningCell ? <LoadingIndicator /> : <IconButton onClick={handleCellRun}><PlayIcon /></IconButton>}
|
||||
</>
|
||||
)}
|
||||
<span className="ml-4">{cell.name}</span>
|
||||
<span className="ml-4">{cell.type === "markdown" ? "Markdown Cell" : cell.name}</span>
|
||||
</div>
|
||||
<div className="pr-4 flex flex-row items-center gap-8">
|
||||
{runCell && (
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
"use client";
|
||||
|
||||
import classNames from "classnames";
|
||||
import { InputHTMLAttributes, useCallback, useEffect, useLayoutEffect, useRef } from "react"
|
||||
import { InputHTMLAttributes, useCallback, useEffect, useRef } from "react"
|
||||
|
||||
interface TextAreaProps extends Omit<InputHTMLAttributes<HTMLTextAreaElement>, "onChange"> {
|
||||
isAutoExpanding?: boolean; // Set to true to enable auto-expanding text area behavior. Default is false.
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
}
|
||||
|
||||
export default function TextArea({
|
||||
|
|
@ -19,95 +19,81 @@ export default function TextArea({
|
|||
placeholder = "",
|
||||
onKeyUp,
|
||||
...props
|
||||
}: TextAreaProps) {
|
||||
const handleTextChange = useCallback((event: Event) => {
|
||||
const fakeTextAreaElement = event.target as HTMLDivElement;
|
||||
const newValue = fakeTextAreaElement.innerText;
|
||||
}: TextAreaProps) {
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const maxHeightRef = useRef<number | null>(null);
|
||||
const throttleTimeoutRef = useRef<number | null>(null);
|
||||
const lastAdjustTimeRef = useRef<number>(0);
|
||||
const THROTTLE_MS = 250; // 4 calculations per second
|
||||
|
||||
const adjustHeight = useCallback(() => {
|
||||
if (!isAutoExpanding || !textareaRef.current) return;
|
||||
|
||||
const textarea = textareaRef.current;
|
||||
|
||||
// Cache maxHeight on first calculation
|
||||
if (maxHeightRef.current === null) {
|
||||
const computedStyle = getComputedStyle(textarea);
|
||||
maxHeightRef.current = computedStyle.maxHeight === "none"
|
||||
? Infinity
|
||||
: parseInt(computedStyle.maxHeight) || Infinity;
|
||||
}
|
||||
|
||||
// Reset height to auto to get the correct scrollHeight
|
||||
textarea.style.height = "auto";
|
||||
// Set height to scrollHeight, but respect max-height
|
||||
const scrollHeight = textarea.scrollHeight;
|
||||
textarea.style.height = `${Math.min(scrollHeight, maxHeightRef.current)}px`;
|
||||
lastAdjustTimeRef.current = Date.now();
|
||||
}, [isAutoExpanding]);
|
||||
|
||||
const handleChange = useCallback((event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const newValue = event.target.value;
|
||||
onChange?.(newValue);
|
||||
}, [onChange]);
|
||||
|
||||
const handleKeyUp = useCallback((event: Event) => {
|
||||
if (onKeyUp) {
|
||||
onKeyUp(event as unknown as React.KeyboardEvent<HTMLTextAreaElement>);
|
||||
}
|
||||
}, [onKeyUp]);
|
||||
// Throttle height adjustments to avoid blocking typing
|
||||
if (isAutoExpanding) {
|
||||
const now = Date.now();
|
||||
const timeSinceLastAdjust = now - lastAdjustTimeRef.current;
|
||||
|
||||
const handleTextAreaFocus = (event: React.FocusEvent<HTMLDivElement>) => {
|
||||
if (event.target.innerText.trim() === placeholder) {
|
||||
event.target.innerText = "";
|
||||
}
|
||||
};
|
||||
const handleTextAreaBlur = (event: React.FocusEvent<HTMLDivElement>) => {
|
||||
if (value === "") {
|
||||
event.target.innerText = placeholder;
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
onChange(event.target.value);
|
||||
};
|
||||
|
||||
const fakeTextAreaRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const fakeTextAreaElement = fakeTextAreaRef.current;
|
||||
|
||||
if (fakeTextAreaElement && fakeTextAreaElement.innerText.trim() !== "") {
|
||||
fakeTextAreaElement.innerText = placeholder;
|
||||
}
|
||||
}, [placeholder]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const fakeTextAreaElement = fakeTextAreaRef.current;
|
||||
|
||||
if (fakeTextAreaElement) {
|
||||
fakeTextAreaElement.addEventListener("input", handleTextChange);
|
||||
fakeTextAreaElement.addEventListener("keyup", handleKeyUp);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (fakeTextAreaElement) {
|
||||
fakeTextAreaElement.removeEventListener("input", handleTextChange);
|
||||
fakeTextAreaElement.removeEventListener("keyup", handleKeyUp);
|
||||
if (timeSinceLastAdjust >= THROTTLE_MS) {
|
||||
adjustHeight();
|
||||
} else {
|
||||
if (throttleTimeoutRef.current !== null) {
|
||||
clearTimeout(throttleTimeoutRef.current);
|
||||
}
|
||||
throttleTimeoutRef.current = window.setTimeout(() => {
|
||||
adjustHeight();
|
||||
throttleTimeoutRef.current = null;
|
||||
}, THROTTLE_MS - timeSinceLastAdjust);
|
||||
}
|
||||
};
|
||||
}, [handleKeyUp, handleTextChange]);
|
||||
}
|
||||
}, [onChange, isAutoExpanding, adjustHeight]);
|
||||
|
||||
useEffect(() => {
|
||||
const fakeTextAreaElement = fakeTextAreaRef.current;
|
||||
const textAreaText = fakeTextAreaElement?.innerText;
|
||||
|
||||
if (fakeTextAreaElement && (value === "" || value === "\n")) {
|
||||
fakeTextAreaElement.innerText = placeholder;
|
||||
return;
|
||||
if (isAutoExpanding && textareaRef.current) {
|
||||
adjustHeight();
|
||||
}
|
||||
}, [value, isAutoExpanding, adjustHeight]);
|
||||
|
||||
if (fakeTextAreaElement && textAreaText !== value) {
|
||||
fakeTextAreaElement.innerText = value;
|
||||
}
|
||||
}, [placeholder, value]);
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (throttleTimeoutRef.current !== null) {
|
||||
clearTimeout(throttleTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return isAutoExpanding ? (
|
||||
<>
|
||||
<div
|
||||
ref={fakeTextAreaRef}
|
||||
contentEditable="true"
|
||||
role="textbox"
|
||||
aria-multiline="true"
|
||||
className={classNames("block w-full rounded-md bg-white px-4 py-4 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600", className)}
|
||||
onFocus={handleTextAreaFocus}
|
||||
onBlur={handleTextAreaBlur}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
return (
|
||||
<textarea
|
||||
ref={isAutoExpanding ? textareaRef : undefined}
|
||||
name={name}
|
||||
style={style}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
className={classNames("block w-full rounded-md bg-white px-4 py-4 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600", className)}
|
||||
onChange={handleChange}
|
||||
onKeyUp={onKeyUp}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function handleServerErrors(response: Response, retry?: (response: Response) => Promise<Response>, useCloud?: boolean): Promise<Response> {
|
||||
export default function handleServerErrors(
|
||||
response: Response,
|
||||
retry: ((response: Response) => Promise<Response>) | null = null,
|
||||
useCloud: boolean = false,
|
||||
): Promise<Response> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (response.status === 401 && !useCloud) {
|
||||
if ((response.status === 401 || response.status === 403) && !useCloud) {
|
||||
if (retry) {
|
||||
return retry(response)
|
||||
.catch(() => {
|
||||
|
|
|
|||
|
|
@ -3,3 +3,4 @@ from .get_notebooks import get_notebooks
|
|||
from .create_notebook import create_notebook
|
||||
from .update_notebook import update_notebook
|
||||
from .delete_notebook import delete_notebook
|
||||
from .create_tutorial_notebooks import create_tutorial_notebooks
|
||||
|
|
|
|||
|
|
@ -6,40 +6,6 @@ from cognee.infrastructure.databases.relational import with_async_session
|
|||
|
||||
from ..models.Notebook import Notebook, NotebookCell
|
||||
|
||||
TUTORIAL_NOTEBOOK_NAME = "Python Development with Cognee Tutorial 🧠"
|
||||
|
||||
|
||||
async def _create_tutorial_notebook(
|
||||
user_id: UUID, session: AsyncSession, force_refresh: bool = False
|
||||
) -> None:
|
||||
"""
|
||||
Create the default tutorial notebook for new users.
|
||||
Dynamically fetches from: https://github.com/topoteretes/cognee/blob/notebook_tutorial/notebooks/starter_tutorial.zip
|
||||
"""
|
||||
TUTORIAL_ZIP_URL = (
|
||||
"https://github.com/topoteretes/cognee/raw/notebook_tutorial/notebooks/starter_tutorial.zip"
|
||||
)
|
||||
|
||||
try:
|
||||
# Create notebook from remote zip file (includes notebook + data files)
|
||||
notebook = await Notebook.from_ipynb_zip_url(
|
||||
zip_url=TUTORIAL_ZIP_URL,
|
||||
owner_id=user_id,
|
||||
notebook_filename="tutorial.ipynb",
|
||||
name=TUTORIAL_NOTEBOOK_NAME,
|
||||
deletable=False,
|
||||
force=force_refresh,
|
||||
)
|
||||
|
||||
# Add to session and commit
|
||||
session.add(notebook)
|
||||
await session.commit()
|
||||
|
||||
except Exception as e:
|
||||
print(f"Failed to fetch tutorial notebook from {TUTORIAL_ZIP_URL}: {e}")
|
||||
|
||||
raise e
|
||||
|
||||
|
||||
@with_async_session
|
||||
async def create_notebook(
|
||||
|
|
|
|||
191
cognee/modules/notebooks/methods/create_tutorial_notebooks.py
Normal file
191
cognee/modules/notebooks/methods/create_tutorial_notebooks.py
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
from pathlib import Path
|
||||
from uuid import NAMESPACE_OID, UUID, uuid5, uuid4
|
||||
from typing import List, Optional, Dict, Any
|
||||
import re
|
||||
import json
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from cognee.shared.logging_utils import get_logger
|
||||
from cognee.root_dir import ROOT_DIR
|
||||
|
||||
from ..models.Notebook import Notebook, NotebookCell
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
def _get_tutorials_directory() -> Path:
|
||||
"""Get the path to the tutorials directory."""
|
||||
return ROOT_DIR / "modules" / "notebooks" / "tutorials"
|
||||
|
||||
|
||||
def _parse_cell_index(filename: str) -> int:
|
||||
"""Extract cell index from filename like 'cell-0.md' or 'cell-123.py'."""
|
||||
match = re.search(r"cell-(\d+)", filename)
|
||||
if match:
|
||||
return int(match.group(1))
|
||||
return -1
|
||||
|
||||
|
||||
def _get_cell_type(file_path: Path) -> str:
|
||||
"""Determine cell type from file extension."""
|
||||
extension = file_path.suffix.lower()
|
||||
if extension == ".md":
|
||||
return "markdown"
|
||||
elif extension == ".py":
|
||||
return "code"
|
||||
else:
|
||||
raise ValueError(f"Unsupported cell file type: {extension}")
|
||||
|
||||
|
||||
def _extract_markdown_heading(content: str) -> str | None:
|
||||
"""Extract the first markdown heading from content."""
|
||||
for line in content.splitlines():
|
||||
line = line.strip()
|
||||
# Match lines starting with one or more # followed by space and text
|
||||
match = re.match(r"^#+\s+(.+)$", line)
|
||||
if match:
|
||||
return match.group(1).strip()
|
||||
return None
|
||||
|
||||
|
||||
def _get_cell_name(cell_file: Path, cell_type: str, content: str) -> str:
|
||||
"""Get the appropriate name for a cell."""
|
||||
if cell_type == "code":
|
||||
return "Code Cell"
|
||||
elif cell_type == "markdown":
|
||||
heading = _extract_markdown_heading(content)
|
||||
if heading:
|
||||
return heading
|
||||
# Fallback to filename stem
|
||||
return cell_file.stem
|
||||
|
||||
|
||||
def _load_tutorial_cells(tutorial_dir: Path) -> List[NotebookCell]:
|
||||
"""Load all cells from a tutorial directory, sorted by cell index."""
|
||||
cells = []
|
||||
|
||||
cell_files = [
|
||||
file_path
|
||||
for file_path in tutorial_dir.iterdir()
|
||||
if file_path.is_file()
|
||||
and file_path.name.startswith("cell-")
|
||||
and file_path.suffix in [".md", ".py"]
|
||||
]
|
||||
|
||||
cell_files.sort(key=lambda file_path: _parse_cell_index(file_path.name))
|
||||
|
||||
for cell_file in cell_files:
|
||||
try:
|
||||
cell_type = _get_cell_type(cell_file)
|
||||
content = cell_file.read_text(encoding="utf-8")
|
||||
cell_name = _get_cell_name(cell_file, cell_type, content)
|
||||
|
||||
cells.append(
|
||||
NotebookCell(
|
||||
id=uuid4(),
|
||||
type=cell_type,
|
||||
name=cell_name,
|
||||
content=content,
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to load cell {cell_file}: {e}")
|
||||
continue
|
||||
|
||||
return cells
|
||||
|
||||
|
||||
def _read_tutorial_config(tutorial_dir: Path) -> Optional[Dict[str, Any]]:
|
||||
"""Read config.json from a tutorial directory if it exists."""
|
||||
config_path = tutorial_dir / "config.json"
|
||||
if config_path.exists():
|
||||
try:
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except (json.JSONDecodeError, IOError) as e:
|
||||
logger.warning(f"Failed to read config.json from {tutorial_dir}: {e}")
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def _format_tutorial_name(tutorial_dir_name: str) -> str:
|
||||
"""Format tutorial directory name into a readable notebook name (fallback)."""
|
||||
|
||||
name = tutorial_dir_name.replace("-", " ").replace("_", " ")
|
||||
return f"{name.capitalize()} - tutorial 🧠"
|
||||
|
||||
|
||||
async def create_tutorial_notebooks(user_id: UUID, session: AsyncSession) -> None:
|
||||
"""
|
||||
Create tutorial notebooks for all tutorials found in the tutorials directory.
|
||||
Each tutorial directory will become a separate notebook.
|
||||
"""
|
||||
try:
|
||||
tutorials_dir = _get_tutorials_directory()
|
||||
|
||||
if not tutorials_dir.exists():
|
||||
logger.warning(f"Tutorials directory not found: {tutorials_dir}")
|
||||
return
|
||||
|
||||
tutorial_dirs = [
|
||||
d for d in tutorials_dir.iterdir() if d.is_dir() and not d.name.startswith(".")
|
||||
]
|
||||
|
||||
if not tutorial_dirs:
|
||||
logger.warning(f"No tutorial directories found in {tutorials_dir}")
|
||||
return
|
||||
|
||||
notebooks_to_add = []
|
||||
|
||||
for tutorial_dir in tutorial_dirs:
|
||||
try:
|
||||
cells = _load_tutorial_cells(tutorial_dir)
|
||||
|
||||
if not cells:
|
||||
logger.warning(f"No cells found in tutorial directory: {tutorial_dir}")
|
||||
continue
|
||||
|
||||
config = _read_tutorial_config(tutorial_dir)
|
||||
|
||||
# Use name from config.json, or fallback to formatted directory name
|
||||
if config and "name" in config:
|
||||
notebook_name = config["name"]
|
||||
else:
|
||||
notebook_name = _format_tutorial_name(tutorial_dir.name)
|
||||
logger.warning(
|
||||
f"No config.json or 'name' field found in {tutorial_dir}, "
|
||||
f"using fallback name: {notebook_name}"
|
||||
)
|
||||
|
||||
# Use deletable flag from config.json, or default to False for tutorials
|
||||
deletable = False
|
||||
if config and "deletable" in config:
|
||||
deletable = bool(config["deletable"])
|
||||
|
||||
notebook_id = uuid5(NAMESPACE_OID, name=notebook_name)
|
||||
|
||||
notebook = Notebook(
|
||||
id=notebook_id,
|
||||
owner_id=user_id,
|
||||
name=notebook_name,
|
||||
cells=cells,
|
||||
deletable=deletable,
|
||||
)
|
||||
|
||||
notebooks_to_add.append(notebook)
|
||||
logger.info(f"Created tutorial notebook: {notebook_name} with {len(cells)} cells")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create tutorial notebook from {tutorial_dir}: {e}")
|
||||
continue
|
||||
|
||||
if not notebooks_to_add:
|
||||
return
|
||||
|
||||
for notebook in notebooks_to_add:
|
||||
session.add(notebook)
|
||||
|
||||
await session.commit()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create tutorial notebooks for user {user_id}: {e}")
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
from uuid import UUID
|
||||
from uuid import NAMESPACE_OID, UUID, uuid5
|
||||
from typing import List
|
||||
from sqlalchemy import select, and_
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
|
@ -6,7 +6,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||
from cognee.infrastructure.databases.relational import with_async_session
|
||||
|
||||
from ..models.Notebook import Notebook
|
||||
from .create_notebook import _create_tutorial_notebook, TUTORIAL_NOTEBOOK_NAME
|
||||
from .create_tutorial_notebooks import create_tutorial_notebooks
|
||||
|
||||
from cognee.shared.logging_utils import get_logger
|
||||
|
||||
|
|
@ -19,21 +19,25 @@ async def get_notebooks(
|
|||
session: AsyncSession,
|
||||
) -> List[Notebook]:
|
||||
# Check if tutorial notebook already exists for this user
|
||||
tutorial_notebook_ids = [
|
||||
uuid5(NAMESPACE_OID, name="Cognee Basics - tutorial 🧠"),
|
||||
uuid5(NAMESPACE_OID, name="Python Development with Cognee - tutorial 🧠"),
|
||||
]
|
||||
tutorial_query = select(Notebook).where(
|
||||
and_(
|
||||
Notebook.owner_id == user_id,
|
||||
Notebook.name == TUTORIAL_NOTEBOOK_NAME,
|
||||
Notebook.id.in_(tutorial_notebook_ids),
|
||||
~Notebook.deletable,
|
||||
)
|
||||
)
|
||||
tutorial_result = await session.execute(tutorial_query)
|
||||
tutorial_notebook = tutorial_result.scalar_one_or_none()
|
||||
tutorial_notebooks = tutorial_result.scalars().all()
|
||||
|
||||
# If tutorial notebook doesn't exist, create it
|
||||
if tutorial_notebook is None:
|
||||
logger.info(f"Tutorial notebook not found for user {user_id}, creating it")
|
||||
# If tutorial notebooks don't exist, create them
|
||||
if len(tutorial_notebooks) == 0:
|
||||
logger.info(f"Tutorial notebooks not found for user {user_id}, creating them")
|
||||
try:
|
||||
await _create_tutorial_notebook(user_id, session, force_refresh=False)
|
||||
await create_tutorial_notebooks(user_id, session)
|
||||
except Exception as e:
|
||||
# Log the error but continue to return existing notebooks
|
||||
logger.error(f"Failed to create tutorial notebook for user {user_id}: {e}")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
# Use Cognee to Build Your Own Knowledge Graph
|
||||
|
||||
Cognee is a tool that allows you to build your own knowledge graph from the data you have.
|
||||
10
cognee/modules/notebooks/tutorials/cognee-basics/cell-2.md
Normal file
10
cognee/modules/notebooks/tutorials/cognee-basics/cell-2.md
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# What You'll Learn in This Tutorial
|
||||
|
||||
In this tutorial, you'll learn how to use Cognee to transform scattered data into an intelligent knowledge system that enhances your workflow.
|
||||
By the end, you'll have:
|
||||
|
||||
- Connected disparate data sources into a unified AI memory graph
|
||||
- Built a memory layer that infers knowledge from provided data
|
||||
- Learn how to use search capabilities that combine the diverse context
|
||||
|
||||
This tutorial demonstrates the power of knowledge graphs and retrieval-augmented generation (RAG), showing you how to build systems that learn from data and infer knowledge.
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# Cognee and Its Core Operations
|
||||
|
||||
Before we dive in, let's understand the core Cognee operations we'll be working with:
|
||||
|
||||
- `cognee.add()` - Ingests raw data into the system
|
||||
- `cognee.cognify()` - Processes and structures data into a knowledge graph using AI
|
||||
- `cognee.search()` - Queries the knowledge graph with natural language
|
||||
28
cognee/modules/notebooks/tutorials/cognee-basics/cell-4.py
Normal file
28
cognee/modules/notebooks/tutorials/cognee-basics/cell-4.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# Add data one by one, or pass a list to add multiple items at once
|
||||
|
||||
await cognee.add(
|
||||
"Harry Potter is a student at Hogwarts and belongs to Gryffindor house. \
|
||||
He is known for defeating Voldemort and his Patronus is a stag.",
|
||||
dataset_name="cognee-basics",
|
||||
)
|
||||
|
||||
await cognee.add(
|
||||
"Hermione Granger is a student at Hogwarts and also belongs to Gryffindor house. \
|
||||
She is known for her intelligence and deep knowledge of spells. Her Patronus is an otter.",
|
||||
dataset_name="cognee-basics",
|
||||
)
|
||||
|
||||
await cognee.add(
|
||||
"Severus Snape is a professor at Hogwarts who teaches Potions. \
|
||||
He belongs to Slytherin house and was secretly loyal to Albus Dumbledore.",
|
||||
dataset_name="cognee-basics",
|
||||
)
|
||||
|
||||
await cognee.add(
|
||||
[
|
||||
"Hogwarts is a magical school located in Scotland. During Harry Potter's time at school, the headmaster was Albus Dumbledore.",
|
||||
"A Horcrux is a dark magic object used to store a fragment of a wizard's soul. Voldemort created multiple Horcruxes to achieve immortality.",
|
||||
"The Elder Wand is a powerful wand believed to be unbeatable. Its final known owner was Harry Potter.",
|
||||
],
|
||||
dataset_name="cognee-basics",
|
||||
)
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Run cognify to process the data and create a knowledge graph
|
||||
|
||||
await cognee.cognify(datasets=["cognee-basics"])
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# And after the cognification, search the knowledge graph
|
||||
|
||||
result = await cognee.search(
|
||||
"Which characters belong to Gryffindor?",
|
||||
datasets=["cognee-basics"],
|
||||
)
|
||||
|
||||
# Print the result so you can see it in the notebook output
|
||||
print(result)
|
||||
17
cognee/modules/notebooks/tutorials/cognee-basics/cell-7.py
Normal file
17
cognee/modules/notebooks/tutorials/cognee-basics/cell-7.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Run multiple searches and print the results
|
||||
|
||||
result_1 = await cognee.search(
|
||||
"Who taught Potions at Hogwarts at time Albus Dumbledore was the headmaster?",
|
||||
datasets=["cognee-basics"],
|
||||
)
|
||||
|
||||
# Print the result so you can see it in the notebook output
|
||||
print(result_1)
|
||||
|
||||
|
||||
result_2 = await cognee.search(
|
||||
"How to defeat Voldemort?",
|
||||
datasets=["cognee-basics"],
|
||||
)
|
||||
|
||||
print(result_2)
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "Cognee Basics - tutorial 🧠",
|
||||
"deletable": false
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Using Cognee with Python Development Data
|
||||
|
||||
Unite authoritative Python practice (Guido van Rossum's own contributions!), normative guidance (Zen/PEP 8), and your lived context (rules + conversations) into one AI memory that produces answers that are relevant, explainable, and consistent.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
You'll see that cognee has connected your Python development challenges with Guido's approaches, revealing patterns like:
|
||||
- "Type hint implementation failed due to circular imports - similar to issue Guido solved in mypy PR #1234"
|
||||
- "Performance bottleneck in list comprehension matches pattern Guido optimized in CPython commit abc123"
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Memify
|
||||
|
||||
Let's now introduce the memory functions. These algorithms run on top of your semantic layer, connecting the dots and improving the reasoning in search.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
memify_result = await cognee.memify(dataset="python-development-with-cognee")
|
||||
|
||||
print(memify_result)
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# What Memify Does in This Case
|
||||
|
||||
This advanced function uses AI to:
|
||||
- Infer rule patterns from your code (e.g., "When implementing iterators, always follow the protocol Guido established")
|
||||
- Connect design philosophy to practice (e.g., linking "explicit is better than implicit" to your type hinting decisions)
|
||||
|
||||
Now let's see how the system has connected your Python development patterns with established best practices:
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
# Search for connections between your async patterns and Python philosophy
|
||||
results = await cognee.search(
|
||||
query_text="How does my AsyncWebScraper implementation align with Python's design principles?",
|
||||
datasets=["python-development-with-cognee"],
|
||||
)
|
||||
print(results)
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Temporal Graphs
|
||||
|
||||
As we used `temporal_cognify` option for each cognification, we can ask time related questions, for example:
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
result = await cognee.search(
|
||||
query_text="What can we learn from Guido's contributions in 2025?",
|
||||
query_type=cognee.SearchType.TEMPORAL,
|
||||
datasets=["python-development-with-cognee"],
|
||||
)
|
||||
|
||||
print(result)
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# What You'll Learn in This Tutorial
|
||||
|
||||
In this comprehensive tutorial, you'll discover how to transform scattered development data into an intelligent knowledge system that enhances your coding workflow. By the end, you'll have:
|
||||
- Connected disparate data sources (Guido's CPython contributions, mypy development, PEP discussions, your Python projects) into a unified AI memory graph
|
||||
- Built a memory layer that understands Python design philosophy, best practice coding patterns, and your preferences and experience
|
||||
- Learned how to use intelligent search capabilities that combine the diverse context
|
||||
- Integrated everything with your coding environment through MCP (Model Context Protocol)
|
||||
|
||||
This tutorial demonstrates the power of knowledge graphs and retrieval-augmented generation (RAG) for software development, showing you how to build systems that learn from Python's creator and improve your own Python development.
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# Cognee and Its Core Operations
|
||||
|
||||
Before we dive in, let's understand the core Cognee operations we'll be working with:
|
||||
- `cognee.add()` - Ingests raw data (files, text, APIs) into the system
|
||||
- `cognee.cognify()` - Processes and structures data into a knowledge graph using AI
|
||||
- `cognee.search()` - Queries the knowledge graph with natural language or Cypher
|
||||
- `cognee.memify()` - Cognee's "secret sauce" that infers implicit connections and rules from your data
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# Data Used in This Tutorial
|
||||
|
||||
Cognee can ingest many types of sources. In this tutorial, we use a small, concrete set of files that cover different perspectives:
|
||||
|
||||
- `guido_contributions.json` — Authoritative exemplars. Real PRs and commits from Guido van Rossum (mypy, CPython). These show how Python’s creator solved problems and provide concrete anchors for patterns.
|
||||
- `pep_style_guide.md` — Norms. Encodes community style and typing conventions (PEP 8 and related). Ensures that search results and inferred rules align with widely accepted standards.
|
||||
- `zen_principles.md` — Philosophy The Zen of Python. Grounds design trade‑offs (simplicity, explicitness, readability) beyond syntax or mechanics.
|
||||
- `my_developer_rules.md` — Local constraints Your house rules, conventions, and project‑specific requirements (scope, privacy, Spec.md). Keeps recommendations relevant to your actual workflow.
|
||||
- `copilot_conversations.json` — Personal history. Transcripts of real assistant conversations, including your questions, code snippets, and discussion topics. Captures “how you code” and connects it to “how Guido codes.”
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# First Data Ingestion: Exploring Guido's Python Contributions
|
||||
|
||||
We'll begin with a document that contains detailed PRs and commits from Guido van Rossum's work on mypy and CPython, showing real-world examples of Python's creator solving type system and language design challenges.
|
||||
|
||||
We'll use Cognee's `add()` and `cognify()` functions to ingest this data and build a knowledge graph that connects Guido's development patterns with Python best practices.
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# Add Guido's contributions to data
|
||||
await cognee.add(
|
||||
"d18g11dwdlgfey.cloudfront.net/tutorials/python-development-with-cognee/data/guido_contributions.json",
|
||||
node_set=["guido_data"],
|
||||
dataset_name="python-development-with-cognee",
|
||||
)
|
||||
|
||||
# Cognify added data into a knowledge graph
|
||||
await cognee.cognify(datasets=["python-development-with-cognee"], temporal_cognify=True)
|
||||
|
||||
# Search the knowledge graph
|
||||
results = await cognee.search("Show me commits", datasets=["python-development-with-cognee"])
|
||||
print(results)
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# What Just Happened?
|
||||
|
||||
The `search()` function uses natural language to query a knowledge graph containing Guido's development history. Unlike traditional databases, Cognee understands the relationships between commits, language features, design decisions, and evolution over time.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Ingesting More Data
|
||||
|
||||
Now we'll add the remaining data and see how the connections emerge between Guido's contributions, Python best practices, and user conversations.
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
await cognee.add(
|
||||
"d18g11dwdlgfey.cloudfront.net/tutorials/python-development-with-cognee/data/copilot_conversations.json",
|
||||
node_set=["developer_data"],
|
||||
dataset_name="python-development-with-cognee",
|
||||
)
|
||||
|
||||
await cognee.add(
|
||||
"d18g11dwdlgfey.cloudfront.net/tutorials/python-development-with-cognee/data/my_developer_rules.md",
|
||||
node_set=["developer_data"],
|
||||
dataset_name="python-development-with-cognee",
|
||||
)
|
||||
|
||||
await cognee.add(
|
||||
"d18g11dwdlgfey.cloudfront.net/tutorials/python-development-with-cognee/data/zen_principles.md",
|
||||
node_set=["principles_data"],
|
||||
dataset_name="python-development-with-cognee",
|
||||
)
|
||||
|
||||
await cognee.add(
|
||||
"d18g11dwdlgfey.cloudfront.net/tutorials/python-development-with-cognee/data/pep_style_guide.md",
|
||||
node_set=["principles_data"],
|
||||
dataset_name="python-development-with-cognee",
|
||||
)
|
||||
|
||||
await cognee.cognify(datasets=["python-development-with-cognee"], temporal_cognify=True)
|
||||
|
||||
results = await cognee.search(
|
||||
"What Python type hinting challenges did I face, and how does Guido approach similar problems in mypy?",
|
||||
datasets=["python-development-with-cognee"],
|
||||
use_combined_context=True, # Used to show reasoning graph visualization
|
||||
)
|
||||
print(results)
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "Python Development with Cognee - tutorial 🧠",
|
||||
"deletable": false
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
{
|
||||
"conversations": [
|
||||
{
|
||||
"id": "conv_001",
|
||||
"timestamp": "2024-01-15T10:30:00Z",
|
||||
"topic": "async/await patterns",
|
||||
"user_query": "I'm building a web scraper that needs to handle thousands of URLs concurrently. What's the best way to structure this with asyncio?",
|
||||
"assistant_response": "For high-concurrency web scraping, you'll want to use asyncio with aiohttp and implement proper semaphore-based rate limiting. Here's a robust pattern:\n\n```python\nimport asyncio\nimport aiohttp\nfrom typing import List, Dict, Any\n\nclass AsyncWebScraper:\n def __init__(self, max_concurrent: int = 100):\n self.semaphore = asyncio.Semaphore(max_concurrent)\n self.session = None\n \n async def __aenter__(self):\n self.session = aiohttp.ClientSession(\n timeout=aiohttp.ClientTimeout(total=30),\n connector=aiohttp.TCPConnector(limit=100)\n )\n return self\n \n async def __aexit__(self, exc_type, exc_val, exc_tb):\n if self.session:\n await self.session.close()\n \n async def fetch_url(self, url: str) -> Dict[str, Any]:\n async with self.semaphore:\n try:\n async with self.session.get(url) as response:\n content = await response.text()\n return {\n 'url': url,\n 'status': response.status,\n 'content': content,\n 'headers': dict(response.headers)\n }\n except Exception as e:\n return {'url': url, 'error': str(e)}\n \n async def scrape_urls(self, urls: List[str]) -> List[Dict[str, Any]]:\n tasks = [self.fetch_url(url) for url in urls]\n return await asyncio.gather(*tasks, return_exceptions=True)\n\n# Usage\nasync def main():\n urls = ['http://example.com'] * 1000 # Your URLs here\n \n async with AsyncWebScraper(max_concurrent=50) as scraper:\n results = await scraper.scrape_urls(urls)\n \n successful = [r for r in results if 'error' not in r]\n print(f'Successfully scraped {len(successful)} URLs')\n\nif __name__ == '__main__':\n asyncio.run(main())\n```\n\nKey points:\n- Semaphore controls concurrency to avoid overwhelming servers\n- Context manager ensures proper cleanup\n- TCPConnector with connection pooling\n- Proper exception handling for failed requests",
|
||||
"code_context": {
|
||||
"file": "scraper.py",
|
||||
"language": "python",
|
||||
"patterns_discussed": ["async/await", "context_managers", "semaphores", "aiohttp", "error_handling"]
|
||||
},
|
||||
"follow_up_questions": [
|
||||
"How do I add retry logic for failed requests?",
|
||||
"What's the best way to parse the scraped HTML content?"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "conv_002",
|
||||
"timestamp": "2024-01-16T14:20:00Z",
|
||||
"topic": "dataclass vs pydantic",
|
||||
"user_query": "When should I use dataclasses vs Pydantic models? I'm building an API and need to handle user input validation.",
|
||||
"assistant_response": "Great question! For API development with input validation, here's when to use each:\n\n**Use Pydantic when:**\n- Building APIs (FastAPI, Django REST)\n- Need runtime validation of external data\n- Want automatic JSON serialization/deserialization\n- Need complex validation rules\n- Working with configuration files\n\n**Use dataclasses when:**\n- Internal data structures\n- Simple data containers\n- Want to stay in stdlib\n- Performance is critical (dataclasses are faster)\n\nFor your API, I'd recommend Pydantic:\n\n```python\nfrom pydantic import BaseModel, validator, Field\nfrom typing import Optional, List\nfrom datetime import datetime\nfrom enum import Enum\n\nclass UserRole(str, Enum):\n ADMIN = 'admin'\n USER = 'user'\n GUEST = 'guest'\n\nclass CreateUserRequest(BaseModel):\n username: str = Field(..., min_length=3, max_length=50, regex=r'^[a-zA-Z0-9_]+$')\n email: str = Field(..., regex=r'^[\\w\\.-]+@[\\w\\.-]+\\.\\w+$')\n password: str = Field(..., min_length=8)\n role: UserRole = UserRole.USER\n tags: Optional[List[str]] = None\n \n @validator('password')\n def validate_password(cls, v):\n if not any(c.isupper() for c in v):\n raise ValueError('Password must contain uppercase letter')\n if not any(c.isdigit() for c in v):\n raise ValueError('Password must contain digit')\n return v\n \n @validator('tags')\n def validate_tags(cls, v):\n if v and len(v) > 10:\n raise ValueError('Too many tags')\n return v\n\nclass User(BaseModel):\n id: int\n username: str\n email: str\n role: UserRole\n created_at: datetime\n is_active: bool = True\n \n class Config:\n orm_mode = True\n json_encoders = { datetime: lambda v: v.isoformat() }\n\n# FastAPI usage\nfrom fastapi import FastAPI, HTTPException\n\napp = FastAPI()\n\n@app.post('/users/', response_model=User)\nasync def create_user(user_data: CreateUserRequest):\n try:\n new_user = await create_user_in_db(user_data)\n return User.from_orm(new_user)\n except Exception as e:\n raise HTTPException(status_code=400, detail=str(e))\n```\n\nPydantic gives you:\n- Automatic request validation\n- Clear error messages\n- Type coercion\n- JSON Schema generation\n- IDE support with type hints",
|
||||
"code_context": {
|
||||
"file": "models.py",
|
||||
"language": "python",
|
||||
"patterns_discussed": ["pydantic", "dataclasses", "validation", "fastapi", "type_hints", "enums"]
|
||||
},
|
||||
"follow_up_questions": [
|
||||
"How do I handle nested validation with Pydantic?",
|
||||
"Can I use Pydantic with SQLAlchemy models?"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "conv_003",
|
||||
"timestamp": "2024-01-17T09:15:00Z",
|
||||
"topic": "testing patterns",
|
||||
"user_query": "I'm struggling with testing async code and database interactions. What's the best approach for pytest with async functions?",
|
||||
"assistant_response": "Testing async code can be tricky! Here's a comprehensive approach using pytest-asyncio and proper test isolation...",
|
||||
"code_context": {
|
||||
"file": "test_users.py",
|
||||
"language": "python",
|
||||
"patterns_discussed": ["pytest", "async_testing", "fixtures", "mocking", "database_testing", "fastapi_testing"]
|
||||
},
|
||||
"follow_up_questions": [
|
||||
"How do I test WebSocket connections?",
|
||||
"What's the best way to test database migrations?"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "conv_004",
|
||||
"timestamp": "2024-01-18T16:45:00Z",
|
||||
"topic": "performance optimization",
|
||||
"user_query": "My FastAPI app is getting slow with large datasets. How can I optimize database queries and response times?",
|
||||
"assistant_response": "Performance optimization in FastAPI apps typically involves several layers...",
|
||||
"code_context": {
|
||||
"file": "optimizations.py",
|
||||
"language": "python",
|
||||
"patterns_discussed": ["performance_optimization", "caching", "database_optimization", "async_patterns", "monitoring"]
|
||||
},
|
||||
"follow_up_questions": [
|
||||
"How do I implement database connection pooling properly?",
|
||||
"What's the best way to handle memory usage with large datasets?"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "conv_005",
|
||||
"timestamp": "2024-01-19T11:30:00Z",
|
||||
"topic": "error handling and logging",
|
||||
"user_query": "I need to implement proper error handling and logging across my Python application. What's the best approach for production-ready error management?",
|
||||
"assistant_response": "Excellent question! Proper error handling and logging are crucial for production applications...",
|
||||
"code_context": {
|
||||
"file": "error_handling.py",
|
||||
"language": "python",
|
||||
"patterns_discussed": ["error_handling", "logging", "exceptions", "middleware", "decorators", "fastapi"]
|
||||
},
|
||||
"follow_up_questions": [
|
||||
"How do I integrate this with external monitoring tools like Sentry?",
|
||||
"What's the best way to handle errors in background tasks?"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"total_conversations": 5,
|
||||
"date_range": "2024-01-15 to 2024-01-19",
|
||||
"topics_covered": [
|
||||
"async/await patterns",
|
||||
"dataclass vs pydantic",
|
||||
"testing patterns",
|
||||
"performance optimization",
|
||||
"error handling and logging"
|
||||
],
|
||||
"code_patterns_discussed": [
|
||||
"asyncio", "aiohttp", "semaphores", "context_managers",
|
||||
"pydantic", "fastapi", "type_hints", "validation",
|
||||
"pytest", "async_testing", "fixtures", "mocking",
|
||||
"performance_optimization", "caching", "database_optimization",
|
||||
"error_handling", "logging", "exceptions", "middleware"
|
||||
],
|
||||
"difficulty_levels": {
|
||||
"beginner": 1,
|
||||
"intermediate": 2,
|
||||
"advanced": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,976 @@
|
|||
[
|
||||
{
|
||||
"type": "pr",
|
||||
"repository": "mypy",
|
||||
"title": "Re-work indirect dependencies",
|
||||
"description": "Wow, this was quite a ride. Indirect dependencies were always supported kind of on best effort. This PR puts them on some principled foundation. It fixes three crashes and three stale types reported. All tests are quite weird/obscure, they are designed to expose the flaws in current logic (plus one test that passes on master, but it covers important corner case, so I add it just in case ). A short summary of various fixes (in arbitrary order):\r\n* Update many outdated comments and docstrings\r\n* Missing transitive dependency is now considered stale\r\n* Handle transitive generic bases in indirection visitor\r\n* Handle chained alias targets in indirection visitor\r\n* Always record original aliases during semantic analysis\r\n* Delete `qualified_tvars` as a concept, they are not needed since long ago\r\n* Remove ad-hoc handling for `TypeInfo`s from `build.py`\r\n* Support symbols with setter type different from getter type\r\n\r\nIn general the logic should be more simple/straightforward now:\r\n* Get all types in a file (need both symbol types _and_ expression types since some types may be only local)\r\n* For each type _transitively_ find all named types in them (thus aggregating all interfaces the type depends on)\r\n* In case any type was forced using `get_proper_type()`, record the orginal type alias during semantic analysis\r\n\r\nNote since this makes the algorithm correct, it may also make it slower (most notably because we must visit generic bases). I tried to offset this by couple optimizations, hopefully performance impact will be minimal.",
|
||||
"url": "https://github.com/python/mypy/pull/19798",
|
||||
"date": "2025-09-05T13:54:52Z",
|
||||
"sha_or_number": "19798",
|
||||
"files_changed": [
|
||||
"mypy/build.py",
|
||||
"mypy/fixup.py",
|
||||
"mypy/indirection.py",
|
||||
"mypy/nodes.py",
|
||||
"mypy/semanal.py",
|
||||
"mypy/server/deps.py",
|
||||
"mypy/test/typefixture.py",
|
||||
"mypy/typeanal.py",
|
||||
"test-data/unit/check-incremental.test"
|
||||
],
|
||||
"additions": 0,
|
||||
"deletions": 0,
|
||||
"labels": [],
|
||||
"related_issues": [],
|
||||
"code_samples": [
|
||||
{
|
||||
"file_path": "mypy/build.py",
|
||||
"language": "python",
|
||||
"before_code": "from mypy.graph_utils import prepare_sccs, strongly_connected_components, topsort\nfrom mypy.indirection import TypeIndirectionVisitor\nfrom mypy.messages import MessageBuilder\nfrom mypy.nodes import Import, ImportAll, ImportBase, ImportFrom, MypyFile, SymbolTable, TypeInfo\nfrom mypy.partially_defined import PossiblyUndefinedVariableVisitor\nfrom mypy.semanal import SemanticAnalyzer\nfrom mypy.semanal_pass1 import SemanticAnalyzerPreAnalysis",
|
||||
"after_code": "from mypy.graph_utils import prepare_sccs, strongly_connected_components, topsort\nfrom mypy.indirection import TypeIndirectionVisitor\nfrom mypy.messages import MessageBuilder\nfrom mypy.nodes import (\n Decorator,\n Import,\n ImportAll,\n ImportBase,\n ImportFrom,\n MypyFile,\n OverloadedFuncDef,\n SymbolTable,\n)\nfrom mypy.partially_defined import PossiblyUndefinedVariableVisitor\nfrom mypy.semanal import SemanticAnalyzer\nfrom mypy.semanal_pass1 import SemanticAnalyzerPreAnalysis",
|
||||
"diff_context": "from mypy.graph_utils import prepare_sccs, strongly_connected_components, topsort\nfrom mypy.indirection import TypeIndirectionVisitor\nfrom mypy.messages import MessageBuilder\nfrom mypy.nodes import Import, ImportAll, ImportBase, ImportFrom, MypyFile, SymbolTable, TypeInfo\nfrom mypy.nodes import (\n Decorator,\n Import,\n ImportAll,\n ImportBase,\n ImportFrom,\n MypyFile,\n OverloadedFuncDef,\n SymbolTable,\n)\nfrom mypy.partially_defined import PossiblyUndefinedVariableVisitor\nfrom mypy.semanal import SemanticAnalyzer\nfrom mypy.semanal_pass1 import SemanticAnalyzerPreAnalysis",
|
||||
"change_type": "modification",
|
||||
"lines_of_context": 6,
|
||||
"function_name": null,
|
||||
"class_name": null,
|
||||
"docstring": null,
|
||||
"coding_patterns": []
|
||||
},
|
||||
{
|
||||
"file_path": "mypy/build.py",
|
||||
"language": "python",
|
||||
"before_code": "\nFor single nodes, processing is simple. If the node was cached, we\ndeserialize the cache data and fix up cross-references. Otherwise, we\ndo semantic analysis followed by type checking. We also handle (c)\nabove; if a module has valid cache data *but* any of its\ndependencies was processed from source, then the module should be\nprocessed from source.\n\nA relatively simple optimization (outside SCCs) we might do in the\nfuture is as follows: if a node's cache data is valid, but one or more\nof its dependencies are out of date so we have to re-parse the node\nfrom source, once we have fully type-checked the node, we can decide\nwhether its symbol table actually changed compared to the cache data\n(by reading the cache data and comparing it to the data we would be\nwriting). If there is no change we can declare the node up to date,\nand any node that depends (and for which we have cached data, and\nwhose other dependencies are up to date) on it won't need to be\nre-parsed from source.\n\nImport cycles\n-------------\n\nFinally we have to decide how to handle (c), import cycles. Here\nwe'll need a modified version of the original state machine\n(build.py), but we only need to do this per SCC, and we won't have to\ndeal with changes to the list of nodes while we're processing it.",
|
||||
"after_code": "\nFor single nodes, processing is simple. If the node was cached, we\ndeserialize the cache data and fix up cross-references. Otherwise, we\ndo semantic analysis followed by type checking. Once we (re-)processed\nan SCC we check whether its interface (symbol table) is still fresh\n(matches previous cached value). If it is not, we consider dependent SCCs\nstale so that they need to be re-parsed as well.\n\nNote on indirect dependencies: normally dependencies are determined from\nimports, but since our type interfaces are \"opaque\" (i.e. symbol tables can\ncontain types identified by name), these are not enough. We *must* also\nadd \"indirect\" dependencies from types to their definitions. For this\npurpose, after we finished processing a module, we travers its type map and\nsymbol tables, and for each type we find (transitively) on which opaque/named\ntypes it depends.\n\nImport cycles\n-------------\n\nFinally we have to decide how to handle (b), import cycles. Here\nwe'll need a modified version of the original state machine\n(build.py), but we only need to do this per SCC, and we won't have to\ndeal with changes to the list of nodes while we're processing it.",
|
||||
"diff_context": "\nFor single nodes, processing is simple. If the node was cached, we\ndeserialize the cache data and fix up cross-references. Otherwise, we\ndo semantic analysis followed by type checking. We also handle (c)\nabove; if a module has valid cache data *but* any of its\ndependencies was processed from source, then the module should be\nprocessed from source.\n\nA relatively simple optimization (outside SCCs) we might do in the\nfuture is as follows: if a node's cache data is valid, but one or more\nof its dependencies are out of date so we have to re-parse the node\nfrom source, once we have fully type-checked the node, we can decide\nwhether its symbol table actually changed compared to the cache data\n(by reading the cache data and comparing it to the data we would be\nwriting). If there is no change we can declare the node up to date,\nand any node that depends (and for which we have cached data, and\nwhose other dependencies are up to date) on it won't need to be\nre-parsed from source.\ndo semantic analysis followed by type checking. Once we (re-)processed\nan SCC we check whether its interface (symbol table) is still fresh\n(matches previous cached value). If it is not, we consider dependent SCCs\nstale so that they need to be re-parsed as well.\n\nNote on indirect dependencies: normally dependencies are determined from\nimports, but since our type interfaces are \"opaque\" (i.e. symbol tables can\ncontain types identified by name), these are not enough. We *must* also\nadd \"indirect\" dependencies from types to their definitions. For this\npurpose, after we finished processing a module, we travers its type map and\nsymbol tables, and for each type we find (transitively) on which opaque/named\ntypes it depends.\n\nImport cycles\n-------------\n\nFinally we have to decide how to handle (c), import cycles. Here\nFinally we have to decide how to handle (b), import cycles. Here\nwe'll need a modified version of the original state machine\n(build.py), but we only need to do this per SCC, and we won't have to\ndeal with changes to the list of nodes while we're processing it.",
|
||||
"change_type": "modification",
|
||||
"lines_of_context": 10,
|
||||
"function_name": null,
|
||||
"class_name": null,
|
||||
"docstring": null,
|
||||
"coding_patterns": [
|
||||
"generator_expression"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file_path": "mypy/build.py",
|
||||
"language": "python",
|
||||
"before_code": "\n # We should always patch indirect dependencies, even in full (non-incremental) builds,\n # because the cache still may be written, and it must be correct.\n # TODO: find a more robust way to traverse *all* relevant types?\n all_types = list(self.type_map().values())\n for _, sym, _ in self.tree.local_definitions():\n if sym.type is not None:\n all_types.append(sym.type)\n if isinstance(sym.node, TypeInfo):\n # TypeInfo symbols have some extra relevant types.\n all_types.extend(sym.node.bases)\n if sym.node.metaclass_type:\n all_types.append(sym.node.metaclass_type)\n if sym.node.typeddict_type:\n all_types.append(sym.node.typeddict_type)\n if sym.node.tuple_type:\n all_types.append(sym.node.tuple_type)\n self._patch_indirect_dependencies(self.type_checker().module_refs, all_types)\n\n if self.options.dump_inference_stats:\n dump_type_stats(",
|
||||
"after_code": "\n # We should always patch indirect dependencies, even in full (non-incremental) builds,\n # because the cache still may be written, and it must be correct.\n all_types = set(self.type_map().values())\n for _, sym, _ in self.tree.local_definitions():\n if sym.type is not None:\n all_types.add(sym.type)\n # Special case: settable properties may have two types.\n if isinstance(sym.node, OverloadedFuncDef) and sym.node.is_property:\n assert isinstance(first_node := sym.node.items[0], Decorator)\n if first_node.var.setter_type:\n all_types.add(first_node.var.setter_type)\n # Using mod_alias_deps is unfortunate but needed, since it is highly impractical\n # (and practically impossible) to avoid all get_proper_type() calls. For example,\n # TypeInfo.bases and metaclass, *args and **kwargs, Overloaded.items, and trivial\n # aliases like Text = str, etc. all currently forced to proper types. Thus, we need\n # to record the original definitions as they are first seen in semanal.py.\n self._patch_indirect_dependencies(\n self.type_checker().module_refs | self.tree.mod_alias_deps, all_types\n )\n\n if self.options.dump_inference_stats:\n dump_type_stats(",
|
||||
"diff_context": "\n # We should always patch indirect dependencies, even in full (non-incremental) builds,\n # because the cache still may be written, and it must be correct.\n # TODO: find a more robust way to traverse *all* relevant types?\n all_types = list(self.type_map().values())\n all_types = set(self.type_map().values())\n for _, sym, _ in self.tree.local_definitions():\n if sym.type is not None:\n all_types.append(sym.type)\n if isinstance(sym.node, TypeInfo):\n # TypeInfo symbols have some extra relevant types.\n all_types.extend(sym.node.bases)\n if sym.node.metaclass_type:\n all_types.append(sym.node.metaclass_type)\n if sym.node.typeddict_type:\n all_types.append(sym.node.typeddict_type)\n if sym.node.tuple_type:\n all_types.append(sym.node.tuple_type)\n self._patch_indirect_dependencies(self.type_checker().module_refs, all_types)\n all_types.add(sym.type)\n # Special case: settable properties may have two types.\n if isinstance(sym.node, OverloadedFuncDef) and sym.node.is_property:\n assert isinstance(first_node := sym.node.items[0], Decorator)\n if first_node.var.setter_type:\n all_types.add(first_node.var.setter_type)\n # Using mod_alias_deps is unfortunate but needed, since it is highly impractical\n # (and practically impossible) to avoid all get_proper_type() calls. For example,\n # TypeInfo.bases and metaclass, *args and **kwargs, Overloaded.items, and trivial\n # aliases like Text = str, etc. all currently forced to proper types. Thus, we need\n # to record the original definitions as they are first seen in semanal.py.\n self._patch_indirect_dependencies(\n self.type_checker().module_refs | self.tree.mod_alias_deps, all_types\n )\n\n if self.options.dump_inference_stats:\n dump_type_stats(",
|
||||
"change_type": "modification",
|
||||
"lines_of_context": 8,
|
||||
"function_name": null,
|
||||
"class_name": null,
|
||||
"docstring": null,
|
||||
"coding_patterns": [
|
||||
"generator_expression"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file_path": "mypy/build.py",
|
||||
"language": "python",
|
||||
"before_code": " self._type_checker.reset()\n self._type_checker = None\n\n def _patch_indirect_dependencies(self, module_refs: set[str], types: list[Type]) -> None:\n assert None not in types\n valid = self.valid_references()\n",
|
||||
"after_code": " self._type_checker.reset()\n self._type_checker = None\n\n def _patch_indirect_dependencies(self, module_refs: set[str], types: set[Type]) -> None:\n assert None not in types\n valid = self.valid_references()\n",
|
||||
"diff_context": " self._type_checker.reset()\n self._type_checker = None\n\n def _patch_indirect_dependencies(self, module_refs: set[str], types: list[Type]) -> None:\n def _patch_indirect_dependencies(self, module_refs: set[str], types: set[Type]) -> None:\n assert None not in types\n valid = self.valid_references()\n",
|
||||
"change_type": "modification",
|
||||
"lines_of_context": 6,
|
||||
"function_name": "_patch_indirect_dependencies",
|
||||
"class_name": null,
|
||||
"docstring": null,
|
||||
"coding_patterns": [
|
||||
"function_definition"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file_path": "mypy/build.py",
|
||||
"language": "python",
|
||||
"before_code": " for id in scc:\n deps.update(graph[id].dependencies)\n deps -= ascc\n stale_deps = {id for id in deps if id in graph and not graph[id].is_interface_fresh()}\n fresh = fresh and not stale_deps\n undeps = set()\n if fresh:",
|
||||
"after_code": " for id in scc:\n deps.update(graph[id].dependencies)\n deps -= ascc\n # Note: if a dependency is not in graph anymore, it should be considered interface-stale.\n # This is important to trigger any relevant updates from indirect dependencies that were\n # removed in load_graph().\n stale_deps = {id for id in deps if id not in graph or not graph[id].is_interface_fresh()}\n fresh = fresh and not stale_deps\n undeps = set()\n if fresh:",
|
||||
"diff_context": " for id in scc:\n deps.update(graph[id].dependencies)\n deps -= ascc\n stale_deps = {id for id in deps if id in graph and not graph[id].is_interface_fresh()}\n # Note: if a dependency is not in graph anymore, it should be considered interface-stale.\n # This is important to trigger any relevant updates from indirect dependencies that were\n # removed in load_graph().\n stale_deps = {id for id in deps if id not in graph or not graph[id].is_interface_fresh()}\n fresh = fresh and not stale_deps\n undeps = set()\n if fresh:",
|
||||
"change_type": "modification",
|
||||
"lines_of_context": 6,
|
||||
"function_name": null,
|
||||
"class_name": null,
|
||||
"docstring": null,
|
||||
"coding_patterns": [
|
||||
"generator_expression"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file_path": "mypy/indirection.py",
|
||||
"language": "python",
|
||||
"before_code": " def __init__(self) -> None:\n # Module references are collected here\n self.modules: set[str] = set()\n # User to avoid infinite recursion with recursive type aliases\n self.seen_aliases: set[types.TypeAliasType] = set()\n # Used to avoid redundant work\n self.seen_fullnames: set[str] = set()\n\n def find_modules(self, typs: Iterable[types.Type]) -> set[str]:\n self.modules = set()\n self.seen_fullnames = set()\n self.seen_aliases = set()\n for typ in typs:\n self._visit(typ)\n return self.modules\n\n def _visit(self, typ: types.Type) -> None:\n if isinstance(typ, types.TypeAliasType):\n # Avoid infinite recursion for recursive type aliases.\n if typ not in self.seen_aliases:\n self.seen_aliases.add(typ)\n typ.accept(self)\n\n def _visit_type_tuple(self, typs: tuple[types.Type, ...]) -> None:\n # Micro-optimization: Specialized version of _visit for lists\n for typ in typs:\n if isinstance(typ, types.TypeAliasType):\n # Avoid infinite recursion for recursive type aliases.\n if typ in self.seen_aliases:\n continue\n self.seen_aliases.add(typ)\n typ.accept(self)\n\n def _visit_type_list(self, typs: list[types.Type]) -> None:\n # Micro-optimization: Specialized version of _visit for tuples\n for typ in typs:\n if isinstance(typ, types.TypeAliasType):\n # Avoid infinite recursion for recursive type aliases.\n if typ in self.seen_aliases:\n continue\n self.seen_aliases.add(typ)\n typ.accept(self)\n\n def _visit_module_name(self, module_name: str) -> None:",
|
||||
"after_code": " def __init__(self) -> None:\n # Module references are collected here\n self.modules: set[str] = set()\n # User to avoid infinite recursion with recursive types\n self.seen_types: set[types.TypeAliasType | types.Instance] = set()\n # Used to avoid redundant work\n self.seen_fullnames: set[str] = set()\n\n def find_modules(self, typs: Iterable[types.Type]) -> set[str]:\n self.modules = set()\n self.seen_fullnames = set()\n self.seen_types = set()\n for typ in typs:\n self._visit(typ)\n return self.modules\n\n def _visit(self, typ: types.Type) -> None:\n # Note: instances are needed for `class str(Sequence[str]): ...`\n if (\n isinstance(typ, types.TypeAliasType)\n or isinstance(typ, types.ProperType)\n and isinstance(typ, types.Instance)\n ):\n # Avoid infinite recursion for recursive types.\n if typ in self.seen_types:\n return\n self.seen_types.add(typ)\n typ.accept(self)\n\n def _visit_type_tuple(self, typs: tuple[types.Type, ...]) -> None:\n # Micro-optimization: Specialized version of _visit for lists\n for typ in typs:\n if (\n isinstance(typ, types.TypeAliasType)\n or isinstance(typ, types.ProperType)\n and isinstance(typ, types.Instance)\n ):\n # Avoid infinite recursion for recursive types.\n if typ in self.seen_types:\n continue\n self.seen_types.add(typ)\n typ.accept(self)\n\n def _visit_type_list(self, typs: list[types.Type]) -> None:\n # Micro-optimization: Specialized version of _visit for tuples\n for typ in typs:\n if (\n isinstance(typ, types.TypeAliasType)\n or isinstance(typ, types.ProperType)\n and isinstance(typ, types.Instance)\n ):\n # Avoid infinite recursion for recursive types.\n if typ in self.seen_types:\n continue\n self.seen_types.add(typ)\n typ.accept(self)\n\n def _visit_module_name(self, module_name: str) -> None:",
|
||||
"diff_context": " def __init__(self) -> None:\n # Module references are collected here\n self.modules: set[str] = set()\n # User to avoid infinite recursion with recursive type aliases\n self.seen_aliases: set[types.TypeAliasType] = set()\n # User to avoid infinite recursion with recursive types\n self.seen_types: set[types.TypeAliasType | types.Instance] = set()\n # Used to avoid redundant work\n self.seen_fullnames: set[str] = set()\n\n def find_modules(self, typs: Iterable[types.Type]) -> set[str]:\n self.modules = set()\n self.seen_fullnames = set()\n self.seen_aliases = set()\n self.seen_types = set()\n for typ in typs:\n self._visit(typ)\n return self.modules\n\n def _visit(self, typ: types.Type) -> None:\n if isinstance(typ, types.TypeAliasType):\n # Avoid infinite recursion for recursive type aliases.\n if typ not in self.seen_aliases:\n self.seen_aliases.add(typ)\n # Note: instances are needed for `class str(Sequence[str]): ...`\n if (\n isinstance(typ, types.TypeAliasType)\n or isinstance(typ, types.ProperType)\n and isinstance(typ, types.Instance)\n ):\n # Avoid infinite recursion for recursive types.\n if typ in self.seen_types:\n return\n self.seen_types.add(typ)\n typ.accept(self)\n\n def _visit_type_tuple(self, typs: tuple[types.Type, ...]) -> None:\n # Micro-optimization: Specialized version of _visit for lists\n for typ in typs:\n if isinstance(typ, types.TypeAliasType):\n # Avoid infinite recursion for recursive type aliases.\n if typ in self.seen_aliases:\n if (\n isinstance(typ, types.TypeAliasType)\n or isinstance(typ, types.ProperType)\n and isinstance(typ, types.Instance)\n ):\n # Avoid infinite recursion for recursive types.\n if typ in self.seen_types:\n continue\n self.seen_aliases.add(typ)\n self.seen_types.add(typ)\n typ.accept(self)\n\n def _visit_type_list(self, typs: list[types.Type]) -> None:\n # Micro-optimization: Specialized version of _visit for tuples\n for typ in typs:\n if isinstance(typ, types.TypeAliasType):\n # Avoid infinite recursion for recursive type aliases.\n if typ in self.seen_aliases:\n if (\n isinstance(typ, types.TypeAliasType)\n or isinstance(typ, types.ProperType)\n and isinstance(typ, types.Instance)\n ):\n # Avoid infinite recursion for recursive types.\n if typ in self.seen_types:\n continue\n self.seen_aliases.add(typ)\n self.seen_types.add(typ)\n typ.accept(self)\n\n def _visit_module_name(self, module_name: str) -> None:",
|
||||
"change_type": "modification",
|
||||
"lines_of_context": 29,
|
||||
"function_name": "_visit_module_name",
|
||||
"class_name": null,
|
||||
"docstring": null,
|
||||
"coding_patterns": [
|
||||
"generator_expression",
|
||||
"context_manager",
|
||||
"class_definition",
|
||||
"type_hint"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file_path": "mypy/indirection.py",
|
||||
"language": "python",
|
||||
"before_code": " self._visit_type_list(t.arg_types)\n\n def visit_instance(self, t: types.Instance) -> None:\n self._visit_type_tuple(t.args)\n if t.type:\n # Uses of a class depend on everything in the MRO,\n # as changes to classes in the MRO can add types to methods,\n # change property types, change the MRO itself, etc.\n for s in t.type.mro:\n self._visit_module_name(s.module_name)\n if t.type.metaclass_type is not None:\n self._visit_module_name(t.type.metaclass_type.type.module_name)\n\n def visit_callable_type(self, t: types.CallableType) -> None:\n self._visit_type_list(t.arg_types)",
|
||||
"after_code": " self._visit_type_list(t.arg_types)\n\n def visit_instance(self, t: types.Instance) -> None:\n # Instance is named, record its definition and continue digging into\n # components that constitute semantic meaning of this type: bases, metaclass,\n # tuple type, and typeddict type.\n # Note: we cannot simply record the MRO, in case an intermediate base contains\n # a reference to type alias, this affects meaning of map_instance_to_supertype(),\n # see e.g. testDoubleReexportGenericUpdated.\n self._visit_type_tuple(t.args)\n if t.type:\n # Important optimization: instead of simply recording the definition and\n # recursing into bases, record the MRO and only traverse generic bases.\n for s in t.type.mro:\n self._visit_module_name(s.module_name)\n for base in s.bases:\n if base.args:\n self._visit_type_tuple(base.args)\n if t.type.metaclass_type:\n self._visit(t.type.metaclass_type)\n if t.type.typeddict_type:\n self._visit(t.type.typeddict_type)\n if t.type.tuple_type:\n self._visit(t.type.tuple_type)\n\n def visit_callable_type(self, t: types.CallableType) -> None:\n self._visit_type_list(t.arg_types)",
|
||||
"diff_context": " self._visit_type_list(t.arg_types)\n\n def visit_instance(self, t: types.Instance) -> None:\n # Instance is named, record its definition and continue digging into\n # components that constitute semantic meaning of this type: bases, metaclass,\n # tuple type, and typeddict type.\n # Note: we cannot simply record the MRO, in case an intermediate base contains\n # a reference to type alias, this affects meaning of map_instance_to_supertype(),\n # see e.g. testDoubleReexportGenericUpdated.\n self._visit_type_tuple(t.args)\n if t.type:\n # Uses of a class depend on everything in the MRO,\n # as changes to classes in the MRO can add types to methods,\n # change property types, change the MRO itself, etc.\n # Important optimization: instead of simply recording the definition and\n # recursing into bases, record the MRO and only traverse generic bases.\n for s in t.type.mro:\n self._visit_module_name(s.module_name)\n if t.type.metaclass_type is not None:\n self._visit_module_name(t.type.metaclass_type.type.module_name)\n for base in s.bases:\n if base.args:\n self._visit_type_tuple(base.args)\n if t.type.metaclass_type:\n self._visit(t.type.metaclass_type)\n if t.type.typeddict_type:\n self._visit(t.type.typeddict_type)\n if t.type.tuple_type:\n self._visit(t.type.tuple_type)\n\n def visit_callable_type(self, t: types.CallableType) -> None:\n self._visit_type_list(t.arg_types)",
|
||||
"change_type": "modification",
|
||||
"lines_of_context": 10,
|
||||
"function_name": "visit_callable_type",
|
||||
"class_name": null,
|
||||
"docstring": null,
|
||||
"coding_patterns": [
|
||||
"generator_expression"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file_path": "mypy/indirection.py",
|
||||
"language": "python",
|
||||
"before_code": " self.seen_fullnames.add(fullname)\n\n def visit_overloaded(self, t: types.Overloaded) -> None:\n self._visit_type_list(list(t.items))\n self._visit(t.fallback)\n\n def visit_tuple_type(self, t: types.TupleType) -> None:",
|
||||
"after_code": " self.seen_fullnames.add(fullname)\n\n def visit_overloaded(self, t: types.Overloaded) -> None:\n for item in t.items:\n self._visit(item)\n self._visit(t.fallback)\n\n def visit_tuple_type(self, t: types.TupleType) -> None:",
|
||||
"diff_context": " self.seen_fullnames.add(fullname)\n\n def visit_overloaded(self, t: types.Overloaded) -> None:\n self._visit_type_list(list(t.items))\n for item in t.items:\n self._visit(item)\n self._visit(t.fallback)\n\n def visit_tuple_type(self, t: types.TupleType) -> None:",
|
||||
"change_type": "modification",
|
||||
"lines_of_context": 6,
|
||||
"function_name": "visit_tuple_type",
|
||||
"class_name": null,
|
||||
"docstring": null,
|
||||
"coding_patterns": []
|
||||
},
|
||||
{
|
||||
"file_path": "mypy/indirection.py",
|
||||
"language": "python",
|
||||
"before_code": " self._visit(t.item)\n\n def visit_type_alias_type(self, t: types.TypeAliasType) -> None:\n self._visit(types.get_proper_type(t))",
|
||||
"after_code": " self._visit(t.item)\n\n def visit_type_alias_type(self, t: types.TypeAliasType) -> None:\n # Type alias is named, record its definition and continue digging into\n # components that constitute semantic meaning of this type: target and args.\n if t.alias:\n self._visit_module_name(t.alias.module)\n self._visit(t.alias.target)\n self._visit_type_list(t.args)",
|
||||
"diff_context": " self._visit(t.item)\n\n def visit_type_alias_type(self, t: types.TypeAliasType) -> None:\n self._visit(types.get_proper_type(t))\n # Type alias is named, record its definition and continue digging into\n # components that constitute semantic meaning of this type: target and args.\n if t.alias:\n self._visit_module_name(t.alias.module)\n self._visit(t.alias.target)\n self._visit_type_list(t.args)",
|
||||
"change_type": "modification",
|
||||
"lines_of_context": 3,
|
||||
"function_name": "visit_type_alias_type",
|
||||
"class_name": null,
|
||||
"docstring": null,
|
||||
"coding_patterns": []
|
||||
},
|
||||
{
|
||||
"file_path": "mypy/nodes.py",
|
||||
"language": "python",
|
||||
"before_code": " defs: list[Statement]\n # Type alias dependencies as mapping from target to set of alias full names\n alias_deps: defaultdict[str, set[str]]\n # Is there a UTF-8 BOM at the start?\n is_bom: bool\n names: SymbolTable",
|
||||
"after_code": " defs: list[Statement]\n # Type alias dependencies as mapping from target to set of alias full names\n alias_deps: defaultdict[str, set[str]]\n # Same as above but for coarse-grained dependencies (i.e. modules instead of full names)\n mod_alias_deps: set[str]\n # Is there a UTF-8 BOM at the start?\n is_bom: bool\n names: SymbolTable",
|
||||
"diff_context": " defs: list[Statement]\n # Type alias dependencies as mapping from target to set of alias full names\n alias_deps: defaultdict[str, set[str]]\n # Same as above but for coarse-grained dependencies (i.e. modules instead of full names)\n mod_alias_deps: set[str]\n # Is there a UTF-8 BOM at the start?\n is_bom: bool\n names: SymbolTable",
|
||||
"change_type": "modification",
|
||||
"lines_of_context": 6,
|
||||
"function_name": null,
|
||||
"class_name": null,
|
||||
"docstring": null,
|
||||
"coding_patterns": [
|
||||
"type_hint"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file_path": "mypy/nodes.py",
|
||||
"language": "python",
|
||||
"before_code": " target: The target type. For generic aliases contains bound type variables\n as nested types (currently TypeVar and ParamSpec are supported).\n _fullname: Qualified name of this type alias. This is used in particular\n to track fine grained dependencies from aliases.\n alias_tvars: Type variables used to define this alias.\n normalized: Used to distinguish between `A = List`, and `A = list`. Both\n are internally stored using `builtins.list` (because `typing.List` is",
|
||||
"after_code": " target: The target type. For generic aliases contains bound type variables\n as nested types (currently TypeVar and ParamSpec are supported).\n _fullname: Qualified name of this type alias. This is used in particular\n to track fine-grained dependencies from aliases.\n module: Module where the alias was defined.\n alias_tvars: Type variables used to define this alias.\n normalized: Used to distinguish between `A = List`, and `A = list`. Both\n are internally stored using `builtins.list` (because `typing.List` is",
|
||||
"diff_context": " target: The target type. For generic aliases contains bound type variables\n as nested types (currently TypeVar and ParamSpec are supported).\n _fullname: Qualified name of this type alias. This is used in particular\n to track fine grained dependencies from aliases.\n to track fine-grained dependencies from aliases.\n module: Module where the alias was defined.\n alias_tvars: Type variables used to define this alias.\n normalized: Used to distinguish between `A = List`, and `A = list`. Both\n are internally stored using `builtins.list` (because `typing.List` is",
|
||||
"change_type": "modification",
|
||||
"lines_of_context": 6,
|
||||
"function_name": null,
|
||||
"class_name": null,
|
||||
"docstring": null,
|
||||
"coding_patterns": []
|
||||
},
|
||||
{
|
||||
"file_path": "mypy/semanal.py",
|
||||
"language": "python",
|
||||
"before_code": " declared_type_vars: TypeVarLikeList | None = None,\n all_declared_type_params_names: list[str] | None = None,\n python_3_12_type_alias: bool = False,\n ) -> tuple[Type | None, list[TypeVarLikeType], set[str], list[str], bool]:\n \"\"\"Check if 'rvalue' is a valid type allowed for aliasing (e.g. not a type variable).\n\n If yes, return the corresponding type, a list of\n qualified type variable names for generic aliases, a set of names the alias depends on,\n and a list of type variables if the alias is generic.\n A schematic example for the dependencies:\n A = int\n B = str\n analyze_alias(Dict[A, B])[2] == {'__main__.A', '__main__.B'}\n \"\"\"\n dynamic = bool(self.function_stack and self.function_stack[-1].is_dynamic())\n global_scope = not self.type and not self.function_stack",
|
||||
"after_code": " declared_type_vars: TypeVarLikeList | None = None,\n all_declared_type_params_names: list[str] | None = None,\n python_3_12_type_alias: bool = False,\n ) -> tuple[Type | None, list[TypeVarLikeType], set[tuple[str, str]], bool]:\n \"\"\"Check if 'rvalue' is a valid type allowed for aliasing (e.g. not a type variable).\n\n If yes, return the corresponding type, a list of type variables for generic aliases,\n a set of names the alias depends on, and True if the original type has empty tuple index.\n An example for the dependencies:\n A = int\n B = str\n analyze_alias(dict[A, B])[2] == {('mod', 'mod.A'), ('mod', 'mod.B')}\n \"\"\"\n dynamic = bool(self.function_stack and self.function_stack[-1].is_dynamic())\n global_scope = not self.type and not self.function_stack",
|
||||
"diff_context": " declared_type_vars: TypeVarLikeList | None = None,\n all_declared_type_params_names: list[str] | None = None,\n python_3_12_type_alias: bool = False,\n ) -> tuple[Type | None, list[TypeVarLikeType], set[str], list[str], bool]:\n ) -> tuple[Type | None, list[TypeVarLikeType], set[tuple[str, str]], bool]:\n \"\"\"Check if 'rvalue' is a valid type allowed for aliasing (e.g. not a type variable).\n\n If yes, return the corresponding type, a list of\n qualified type variable names for generic aliases, a set of names the alias depends on,\n and a list of type variables if the alias is generic.\n A schematic example for the dependencies:\n If yes, return the corresponding type, a list of type variables for generic aliases,\n a set of names the alias depends on, and True if the original type has empty tuple index.\n An example for the dependencies:\n A = int\n B = str\n analyze_alias(Dict[A, B])[2] == {'__main__.A', '__main__.B'}\n analyze_alias(dict[A, B])[2] == {('mod', 'mod.A'), ('mod', 'mod.B')}\n \"\"\"\n dynamic = bool(self.function_stack and self.function_stack[-1].is_dynamic())\n global_scope = not self.type and not self.function_stack",
|
||||
"change_type": "modification",
|
||||
"lines_of_context": 10,
|
||||
"function_name": null,
|
||||
"class_name": null,
|
||||
"docstring": "\"\"\"Check if 'rvalue' is a valid type allowed for aliasing (e.g. not a type variable).",
|
||||
"coding_patterns": [
|
||||
"list_comprehension"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file_path": "mypy/semanal.py",
|
||||
"language": "python",
|
||||
"before_code": " self.cur_mod_node.plugin_deps.setdefault(trigger, set()).add(target)\n\n def add_type_alias_deps(\n self, aliases_used: Collection[str], target: str | None = None\n ) -> None:\n \"\"\"Add full names of type aliases on which the current node depends.\n\n This is used by fine-grained incremental mode to re-check the corresponding nodes.\n If `target` is None, then the target node used will be the current scope.\n \"\"\"\n if not aliases_used:\n # A basic optimization to avoid adding targets with no dependencies to\n # the `alias_deps` dict.\n return\n if target is None:\n target = self.scope.current_target()\n self.cur_mod_node.alias_deps[target].update(aliases_used)\n\n def is_mangled_global(self, name: str) -> bool:\n # A global is mangled if there exists at least one renamed variant.",
|
||||
"after_code": " self.cur_mod_node.plugin_deps.setdefault(trigger, set()).add(target)\n\n def add_type_alias_deps(\n self, aliases_used: Collection[tuple[str, str]], target: str | None = None\n ) -> None:\n \"\"\"Add full names of type aliases on which the current node depends.\n\n This is used by fine-grained incremental mode to re-check the corresponding nodes.\n If `target` is None, then the target node used will be the current scope. For\n coarse-grained mode, add just the module names where aliases are defined.\n \"\"\"\n if not aliases_used:\n return\n if target is None:\n target = self.scope.current_target()\n for mod, fn in aliases_used:\n self.cur_mod_node.alias_deps[target].add(fn)\n self.cur_mod_node.mod_alias_deps.add(mod)\n\n def is_mangled_global(self, name: str) -> bool:\n # A global is mangled if there exists at least one renamed variant.",
|
||||
"diff_context": " self.cur_mod_node.plugin_deps.setdefault(trigger, set()).add(target)\n\n def add_type_alias_deps(\n self, aliases_used: Collection[str], target: str | None = None\n self, aliases_used: Collection[tuple[str, str]], target: str | None = None\n ) -> None:\n \"\"\"Add full names of type aliases on which the current node depends.\n\n This is used by fine-grained incremental mode to re-check the corresponding nodes.\n If `target` is None, then the target node used will be the current scope.\n If `target` is None, then the target node used will be the current scope. For\n coarse-grained mode, add just the module names where aliases are defined.\n \"\"\"\n if not aliases_used:\n # A basic optimization to avoid adding targets with no dependencies to\n # the `alias_deps` dict.\n return\n if target is None:\n target = self.scope.current_target()\n self.cur_mod_node.alias_deps[target].update(aliases_used)\n for mod, fn in aliases_used:\n self.cur_mod_node.alias_deps[target].add(fn)\n self.cur_mod_node.mod_alias_deps.add(mod)\n\n def is_mangled_global(self, name: str) -> bool:\n # A global is mangled if there exists at least one renamed variant.",
|
||||
"change_type": "modification",
|
||||
"lines_of_context": 15,
|
||||
"function_name": "is_mangled_global",
|
||||
"class_name": null,
|
||||
"docstring": "\"\"\"Add full names of type aliases on which the current node depends.",
|
||||
"coding_patterns": [
|
||||
"list_comprehension"
|
||||
]
|
||||
}
|
||||
],
|
||||
"commit_message_style": "concise_subject",
|
||||
"python_version": null,
|
||||
"pep_status": null
|
||||
},
|
||||
{
|
||||
"type": "pr",
|
||||
"repository": "mypy",
|
||||
"title": "chore: add cline_docs/ to .gitignore",
|
||||
"description": "Cline is a commonly used LLM tool which, under certain conditions, creates a cline_docs/ folder with task status and todo items etc\r\n\r\nThis folder is only helpful locally (unless we decide we want to add actual guidelines for Cline here, but thats outside the scope of this PR) so this PR adds it to .gitignore\r\n\r\n<!-- If this pull request fixes an issue, add \"Fixes #NNN\" with the issue number. -->\r\n\r\n<!--\r\nChecklist:\r\n- Read the [Contributing Guidelines](https://github.com/python/mypy/blob/master/CONTRIBUTING.md)\r\n- Add tests for all changed behaviour.\r\n- If you can't add a test, please explain why and how you verified your changes work.\r\n- Make sure CI passes.\r\n- Please do not force push to the PR once it has been reviewed.\r\n-->\r\n",
|
||||
"url": "https://github.com/python/mypy/pull/19797",
|
||||
"date": "2025-09-05T02:35:14Z",
|
||||
"sha_or_number": "19797",
|
||||
"files_changed": [
|
||||
".gitignore"
|
||||
],
|
||||
"additions": 0,
|
||||
"deletions": 0,
|
||||
"labels": [],
|
||||
"related_issues": [],
|
||||
"code_samples": [],
|
||||
"commit_message_style": "concise_subject",
|
||||
"python_version": null,
|
||||
"pep_status": null
|
||||
},
|
||||
{
|
||||
"type": "pr",
|
||||
"repository": "mypy",
|
||||
"title": "[mypyc] Add type annotations to tests",
|
||||
"description": "Missing type annotations can compromise test coverage. My eventual goal is to require annotations by default in all run tests.\r\n",
|
||||
"url": "https://github.com/python/mypy/pull/19794",
|
||||
"date": "2025-09-04T15:56:30Z",
|
||||
"sha_or_number": "19794",
|
||||
"files_changed": [
|
||||
"mypyc/test-data/fixtures/ir.py",
|
||||
"mypyc/test-data/fixtures/typing-full.pyi",
|
||||
"mypyc/test-data/run-dunders.test",
|
||||
"mypyc/test-data/run-singledispatch.test"
|
||||
],
|
||||
"additions": 0,
|
||||
"deletions": 0,
|
||||
"labels": [],
|
||||
"related_issues": [],
|
||||
"code_samples": [
|
||||
{
|
||||
"file_path": "mypyc/test-data/fixtures/ir.py",
|
||||
"language": "python",
|
||||
"before_code": " def __iadd__(self, value: Iterable[_T], /) -> List[_T]: ... # type: ignore[misc]\n def append(self, x: _T) -> None: pass\n def pop(self, i: int = -1) -> _T: pass\n def count(self, _T) -> int: pass\n def extend(self, l: Iterable[_T]) -> None: pass\n def insert(self, i: int, x: _T) -> None: pass\n def sort(self) -> None: pass",
|
||||
"after_code": " def __iadd__(self, value: Iterable[_T], /) -> List[_T]: ... # type: ignore[misc]\n def append(self, x: _T) -> None: pass\n def pop(self, i: int = -1) -> _T: pass\n def count(self, x: _T) -> int: pass\n def extend(self, l: Iterable[_T]) -> None: pass\n def insert(self, i: int, x: _T) -> None: pass\n def sort(self) -> None: pass",
|
||||
"diff_context": " def __iadd__(self, value: Iterable[_T], /) -> List[_T]: ... # type: ignore[misc]\n def append(self, x: _T) -> None: pass\n def pop(self, i: int = -1) -> _T: pass\n def count(self, _T) -> int: pass\n def count(self, x: _T) -> int: pass\n def extend(self, l: Iterable[_T]) -> None: pass\n def insert(self, i: int, x: _T) -> None: pass\n def sort(self) -> None: pass",
|
||||
"change_type": "modification",
|
||||
"lines_of_context": 6,
|
||||
"function_name": "sort",
|
||||
"class_name": null,
|
||||
"docstring": null,
|
||||
"coding_patterns": [
|
||||
"function_definition",
|
||||
"type_hint"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file_path": "mypyc/test-data/fixtures/ir.py",
|
||||
"language": "python",
|
||||
"before_code": "def id(o: object) -> int: pass\n# This type is obviously wrong but the test stubs don't have Sized anymore\ndef len(o: object) -> int: pass\ndef print(*object) -> None: pass\ndef isinstance(x: object, t: object) -> bool: pass\ndef iter(i: Iterable[_T]) -> Iterator[_T]: pass\n@overload",
|
||||
"after_code": "def id(o: object) -> int: pass\n# This type is obviously wrong but the test stubs don't have Sized anymore\ndef len(o: object) -> int: pass\ndef print(*args: object) -> None: pass\ndef isinstance(x: object, t: object) -> bool: pass\ndef iter(i: Iterable[_T]) -> Iterator[_T]: pass\n@overload",
|
||||
"diff_context": "def id(o: object) -> int: pass\n# This type is obviously wrong but the test stubs don't have Sized anymore\ndef len(o: object) -> int: pass\ndef print(*object) -> None: pass\ndef print(*args: object) -> None: pass\ndef isinstance(x: object, t: object) -> bool: pass\ndef iter(i: Iterable[_T]) -> Iterator[_T]: pass\n@overload",
|
||||
"change_type": "modification",
|
||||
"lines_of_context": 6,
|
||||
"function_name": "iter",
|
||||
"class_name": null,
|
||||
"docstring": null,
|
||||
"coding_patterns": [
|
||||
"function_definition",
|
||||
"type_hint"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file_path": "mypyc/test-data/fixtures/typing-full.pyi",
|
||||
"language": "python",
|
||||
"before_code": "class GenericMeta(type): pass\n\nclass _SpecialForm:\n def __getitem__(self, index): ...\nclass TypeVar:\n def __init__(self, name, *args, bound=None): ...\n def __or__(self, other): ...\n\ncast = 0\noverload = 0",
|
||||
"after_code": "class GenericMeta(type): pass\n\nclass _SpecialForm:\n def __getitem__(self, index: Any) -> Any: ...\nclass TypeVar:\n def __init__(self, name: str, *args: Any, bound: Any = None): ...\n def __or__(self, other: Any) -> Any: ...\n\ncast = 0\noverload = 0",
|
||||
"diff_context": "class GenericMeta(type): pass\n\nclass _SpecialForm:\n def __getitem__(self, index): ...\n def __getitem__(self, index: Any) -> Any: ...\nclass TypeVar:\n def __init__(self, name, *args, bound=None): ...\n def __or__(self, other): ...\n def __init__(self, name: str, *args: Any, bound: Any = None): ...\n def __or__(self, other: Any) -> Any: ...\n\ncast = 0\noverload = 0",
|
||||
"change_type": "modification",
|
||||
"lines_of_context": 7,
|
||||
"function_name": "__or__",
|
||||
"class_name": "TypeVar",
|
||||
"docstring": null,
|
||||
"coding_patterns": [
|
||||
"function_definition",
|
||||
"type_hint"
|
||||
]
|
||||
}
|
||||
],
|
||||
"commit_message_style": "concise_subject",
|
||||
"python_version": null,
|
||||
"pep_status": null
|
||||
},
|
||||
{
|
||||
"type": "pr",
|
||||
"repository": "mypy",
|
||||
"title": "Check functions without annotations in mypyc tests",
|
||||
"description": "c.f. https://github.com/python/mypy/pull/19217#discussion_r2314303410\r\n\r\nDisallowing functions without annotations (where not relevant to the tests) is probably a good idea, but this creates a large number of failures which would take some time to go through (many due to common issues, like untyped functions in the fixtures).\r\n\r\nAs a smaller step in the right direction, this sets `check_untyped_defs = True` for the `run-*` tests so that we at least check functions without annotations. ",
|
||||
"url": "https://github.com/python/mypy/pull/19792",
|
||||
"date": "2025-09-04T14:42:17Z",
|
||||
"sha_or_number": "19792",
|
||||
"files_changed": [
|
||||
"mypyc/test-data/fixtures/ir.py",
|
||||
"mypyc/test-data/run-classes.test",
|
||||
"mypyc/test/test_run.py"
|
||||
],
|
||||
"additions": 0,
|
||||
"deletions": 0,
|
||||
"labels": [],
|
||||
"related_issues": [],
|
||||
"code_samples": [
|
||||
{
|
||||
"file_path": "mypyc/test-data/fixtures/ir.py",
|
||||
"language": "python",
|
||||
"before_code": "class type:\n def __init__(self, o: object) -> None: ...\n def __or__(self, o: object) -> Any: ...\n __name__ : str\n __annotations__: Dict[str, Any]\n",
|
||||
"after_code": "class type:\n def __init__(self, o: object) -> None: ...\n def __or__(self, o: object) -> Any: ...\n def __new__(cls, *args: object) -> Any: ...\n __name__ : str\n __annotations__: Dict[str, Any]\n",
|
||||
"diff_context": "class type:\n def __init__(self, o: object) -> None: ...\n def __or__(self, o: object) -> Any: ...\n def __new__(cls, *args: object) -> Any: ...\n __name__ : str\n __annotations__: Dict[str, Any]\n",
|
||||
"change_type": "modification",
|
||||
"lines_of_context": 6,
|
||||
"function_name": "__new__",
|
||||
"class_name": "type",
|
||||
"docstring": null,
|
||||
"coding_patterns": [
|
||||
"function_definition"
|
||||
]
|
||||
}
|
||||
],
|
||||
"commit_message_style": "concise_subject",
|
||||
"python_version": null,
|
||||
"pep_status": null
|
||||
},
|
||||
{
|
||||
"type": "pr",
|
||||
"repository": "mypy",
|
||||
"title": "fix: Allow instantiation of type[None] in analyze_type_type_callee",
|
||||
"description": "<!-- If this pull request fixes an issue, add \"Fixes #NNN\" with the issue number. -->\r\n\r\n(Explain how this PR changes mypy.)\r\n\r\n<!--\r\nChecklist:\r\n- Read the [Contributing Guidelines](https://github.com/python/mypy/blob/master/CONTRIBUTING.md)\r\n- Add tests for all changed behaviour.\r\n- If you can't add a test, please explain why and how you verified your changes work.\r\n- Make sure CI passes.\r\n- Please do not force push to the PR once it has been reviewed.\r\n-->\r\n\r\nFixes #19660\r\n\r\nAllow instantiation of NoneType in type checker\r\n\r\nThis change fixes the error \"Cannot instantiate type 'Type[None]'\"\r\nwhen calling NoneType() or type(None)().\r\n\r\nBy treating NoneType as a callable that returns None, mypy can now correctly\r\nhandle such calls without raising spurious errors.\r\n\r\nAlso, I added test case testTypeUsingTypeCNoneType covering:\r\n- direct calls to type(None)() and NoneType()\r\n- functions accepting type[None] and type[NoneType] parameters and invoking them\r\n\r\nThis ensures proper handling of NoneType instantiation and prevents spurious errors.",
|
||||
"url": "https://github.com/python/mypy/pull/19782",
|
||||
"date": "2025-09-02T06:13:12Z",
|
||||
"sha_or_number": "19782",
|
||||
"files_changed": [
|
||||
"mypy/checkexpr.py",
|
||||
"test-data/unit/check-classes.test"
|
||||
],
|
||||
"additions": 0,
|
||||
"deletions": 0,
|
||||
"labels": [],
|
||||
"related_issues": [
|
||||
"19660"
|
||||
],
|
||||
"code_samples": [
|
||||
{
|
||||
"file_path": "mypy/checkexpr.py",
|
||||
"language": "python",
|
||||
"before_code": " return self.analyze_type_type_callee(tuple_fallback(item), context)\n if isinstance(item, TypedDictType):\n return self.typeddict_callable_from_context(item)\n\n self.msg.unsupported_type_type(item, context)\n return AnyType(TypeOfAny.from_error)",
|
||||
"after_code": " return self.analyze_type_type_callee(tuple_fallback(item), context)\n if isinstance(item, TypedDictType):\n return self.typeddict_callable_from_context(item)\n if isinstance(item, NoneType):\n # NoneType() returns None, so treat it as a callable that returns None\n return CallableType(\n arg_types=[],\n arg_kinds=[],\n arg_names=[],\n ret_type=NoneType(),\n fallback=self.named_type(\"builtins.function\"),\n name=None,\n from_type_type=True,\n )\n\n self.msg.unsupported_type_type(item, context)\n return AnyType(TypeOfAny.from_error)",
|
||||
"diff_context": " return self.analyze_type_type_callee(tuple_fallback(item), context)\n if isinstance(item, TypedDictType):\n return self.typeddict_callable_from_context(item)\n if isinstance(item, NoneType):\n # NoneType() returns None, so treat it as a callable that returns None\n return CallableType(\n arg_types=[],\n arg_kinds=[],\n arg_names=[],\n ret_type=NoneType(),\n fallback=self.named_type(\"builtins.function\"),\n name=None,\n from_type_type=True,\n )\n\n self.msg.unsupported_type_type(item, context)\n return AnyType(TypeOfAny.from_error)",
|
||||
"change_type": "modification",
|
||||
"lines_of_context": 6,
|
||||
"function_name": null,
|
||||
"class_name": null,
|
||||
"docstring": null,
|
||||
"coding_patterns": []
|
||||
}
|
||||
],
|
||||
"commit_message_style": "standard",
|
||||
"python_version": null,
|
||||
"pep_status": null
|
||||
},
|
||||
{
|
||||
"type": "pr",
|
||||
"repository": "mypy",
|
||||
"title": "feat: new mypyc primitives for weakref.proxy",
|
||||
"description": "This PR adds 2 new weakref primitives for weakref.proxy (1 and 2 arg)\r\n\r\nThe C code generates correctly, but I'm not entirely sure why this test is failing. The weakly-proxied object is being destroyed too early, while there should still be a strong reference to it. It also fails if we use the builtin weakref.proxy, so I believe this might be exposing a reference counting bug unrelated to this PR.\r\n\r\n<!--\r\nChecklist:\r\n- Read the [Contributing Guidelines](https://github.com/python/mypy/blob/master/CONTRIBUTING.md)\r\n- Add tests for all changed behaviour.\r\n- If you can't add a test, please explain why and how you verified your changes work.\r\n- Make sure CI passes.\r\n- Please do not force push to the PR once it has been reviewed.\r\n-->\r\n",
|
||||
"url": "https://github.com/python/mypy/pull/19217",
|
||||
"date": "2025-06-03T17:02:26Z",
|
||||
"sha_or_number": "19217",
|
||||
"files_changed": [
|
||||
"mypyc/primitives/weakref_ops.py",
|
||||
"mypyc/test-data/fixtures/ir.py",
|
||||
"mypyc/test-data/irbuild-weakref.test",
|
||||
"mypyc/test-data/run-weakref.test",
|
||||
"test-data/unit/lib-stub/_weakref.pyi",
|
||||
"test-data/unit/lib-stub/weakref.pyi"
|
||||
],
|
||||
"additions": 0,
|
||||
"deletions": 0,
|
||||
"labels": [],
|
||||
"related_issues": [],
|
||||
"code_samples": [
|
||||
{
|
||||
"file_path": "mypyc/test-data/fixtures/ir.py",
|
||||
"language": "python",
|
||||
"before_code": "class UnicodeEncodeError(RuntimeError): pass\nclass UnicodeDecodeError(RuntimeError): pass\nclass NotImplementedError(RuntimeError): pass\n\nclass StopIteration(Exception):\n value: Any",
|
||||
"after_code": "class UnicodeEncodeError(RuntimeError): pass\nclass UnicodeDecodeError(RuntimeError): pass\nclass NotImplementedError(RuntimeError): pass\nclass ReferenceError(Exception): pass\n\nclass StopIteration(Exception):\n value: Any",
|
||||
"diff_context": "class UnicodeEncodeError(RuntimeError): pass\nclass UnicodeDecodeError(RuntimeError): pass\nclass NotImplementedError(RuntimeError): pass\nclass ReferenceError(Exception): pass\n\nclass StopIteration(Exception):\n value: Any",
|
||||
"change_type": "modification",
|
||||
"lines_of_context": 6,
|
||||
"function_name": null,
|
||||
"class_name": "StopIteration",
|
||||
"docstring": null,
|
||||
"coding_patterns": [
|
||||
"class_definition",
|
||||
"type_hint"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file_path": "test-data/unit/lib-stub/_weakref.pyi",
|
||||
"language": "python",
|
||||
"before_code": "",
|
||||
"after_code": "from typing import Any, Callable, TypeVar, overload\nfrom weakref import CallableProxyType, ProxyType\n\n_C = TypeVar(\"_C\", bound=Callable[..., Any])\n_T = TypeVar(\"_T\")\n\n# Return CallableProxyType if object is callable, ProxyType otherwise\n@overload\ndef proxy(object: _C, callback: Callable[[CallableProxyType[_C]], Any] | None = None, /) -> CallableProxyType[_C]: ...\n@overload\ndef proxy(object: _T, callback: Callable[[ProxyType[_T]], Any] | None = None, /) -> ProxyType[_T]: ...",
|
||||
"diff_context": "from typing import Any, Callable, TypeVar, overload\nfrom weakref import CallableProxyType, ProxyType\n\n_C = TypeVar(\"_C\", bound=Callable[..., Any])\n_T = TypeVar(\"_T\")\n\n# Return CallableProxyType if object is callable, ProxyType otherwise\n@overload\ndef proxy(object: _C, callback: Callable[[CallableProxyType[_C]], Any] | None = None, /) -> CallableProxyType[_C]: ...\n@overload\ndef proxy(object: _T, callback: Callable[[ProxyType[_T]], Any] | None = None, /) -> ProxyType[_T]: ...",
|
||||
"change_type": "addition",
|
||||
"lines_of_context": 0,
|
||||
"function_name": "proxy",
|
||||
"class_name": null,
|
||||
"docstring": null,
|
||||
"coding_patterns": [
|
||||
"decorator",
|
||||
"function_definition"
|
||||
]
|
||||
}
|
||||
],
|
||||
"commit_message_style": "concise_subject",
|
||||
"python_version": null,
|
||||
"pep_status": null
|
||||
},
|
||||
{
|
||||
"type": "commit",
|
||||
"repository": "cpython",
|
||||
"title": "gh-128307: Update what's new in 3.13 and 3.14 with create_task changes of asyncio (#134304)",
|
||||
"description": "gh-128307: Update what's new in 3.13 and 3.14 with create_task changes of asyncio (#134304)\n\nCo-authored-by: Adam Turner <9087854+AA-Turner@users.noreply.github.com>",
|
||||
"url": "https://github.com/python/cpython/commit/28625d4f956f8d30671aba1daaac9735932983db",
|
||||
"date": "2025-05-20T08:41:22Z",
|
||||
"sha_or_number": "28625d4f956f8d30671aba1daaac9735932983db",
|
||||
"files_changed": [
|
||||
"Doc/whatsnew/3.13.rst",
|
||||
"Doc/whatsnew/3.14.rst"
|
||||
],
|
||||
"additions": 34,
|
||||
"deletions": 0,
|
||||
"labels": [],
|
||||
"related_issues": [
|
||||
"134304"
|
||||
],
|
||||
"code_samples": [
|
||||
{
|
||||
"file_path": "Doc/whatsnew/3.13.rst",
|
||||
"language": "restructuredtext",
|
||||
"before_code": " never awaited).\n (Contributed by Arthur Tacca and Jason Zhang in :gh:`115957`.)\n\n\nbase64\n------",
|
||||
"after_code": " never awaited).\n (Contributed by Arthur Tacca and Jason Zhang in :gh:`115957`.)\n\n* The function and methods named ``create_task`` have received a new\n ``**kwargs`` argument that is passed through to the task constructor.\n This change was accidentally added in 3.13.3,\n and broke the API contract for custom task factories.\n Several third-party task factories implemented workarounds for this.\n In 3.13.4 and later releases the old factory contract is honored\n once again (until 3.14).\n To keep the workarounds working, the extra ``**kwargs`` argument still\n allows passing additional keyword arguments to :class:`~asyncio.Task`\n and to custom task factories.\n\n This affects the following function and methods:\n :meth:`asyncio.create_task`,\n :meth:`asyncio.loop.create_task`,\n :meth:`asyncio.TaskGroup.create_task`.\n (Contributed by Thomas Grainger in :gh:`128307`.)\n\nbase64\n------",
|
||||
"diff_context": " never awaited).\n (Contributed by Arthur Tacca and Jason Zhang in :gh:`115957`.)\n\n* The function and methods named ``create_task`` have received a new\n ``**kwargs`` argument that is passed through to the task constructor.\n This change was accidentally added in 3.13.3,\n and broke the API contract for custom task factories.\n Several third-party task factories implemented workarounds for this.\n In 3.13.4 and later releases the old factory contract is honored\n once again (until 3.14).\n To keep the workarounds working, the extra ``**kwargs`` argument still\n allows passing additional keyword arguments to :class:`~asyncio.Task`\n and to custom task factories.\n\n This affects the following function and methods:\n :meth:`asyncio.create_task`,\n :meth:`asyncio.loop.create_task`,\n :meth:`asyncio.TaskGroup.create_task`.\n (Contributed by Thomas Grainger in :gh:`128307`.)\n\nbase64\n------",
|
||||
"change_type": "modification",
|
||||
"lines_of_context": 6,
|
||||
"function_name": null,
|
||||
"class_name": null,
|
||||
"docstring": null,
|
||||
"coding_patterns": []
|
||||
},
|
||||
{
|
||||
"file_path": "Doc/whatsnew/3.14.rst",
|
||||
"language": "restructuredtext",
|
||||
"before_code": " (Contributed by Semyon Moroz in :gh:`133367`.)\n\n\nbdb\n---\n",
|
||||
"after_code": " (Contributed by Semyon Moroz in :gh:`133367`.)\n\n\nasyncio\n-------\n\n* The function and methods named :func:`!create_task` now take an arbitrary\n list of keyword arguments. All keyword arguments are passed to the\n :class:`~asyncio.Task` constructor or the custom task factory.\n (See :meth:`~asyncio.loop.set_task_factory` for details.)\n The ``name`` and ``context`` keyword arguments are no longer special;\n the name should now be set using the ``name`` keyword argument of the factory,\n and ``context`` may be ``None``.\n\n This affects the following function and methods:\n :meth:`asyncio.create_task`,\n :meth:`asyncio.loop.create_task`,\n :meth:`asyncio.TaskGroup.create_task`.\n (Contributed by Thomas Grainger in :gh:`128307`.)\n\n\nbdb\n---\n",
|
||||
"diff_context": " (Contributed by Semyon Moroz in :gh:`133367`.)\n\n\nasyncio\n-------\n\n* The function and methods named :func:`!create_task` now take an arbitrary\n list of keyword arguments. All keyword arguments are passed to the\n :class:`~asyncio.Task` constructor or the custom task factory.\n (See :meth:`~asyncio.loop.set_task_factory` for details.)\n The ``name`` and ``context`` keyword arguments are no longer special;\n the name should now be set using the ``name`` keyword argument of the factory,\n and ``context`` may be ``None``.\n\n This affects the following function and methods:\n :meth:`asyncio.create_task`,\n :meth:`asyncio.loop.create_task`,\n :meth:`asyncio.TaskGroup.create_task`.\n (Contributed by Thomas Grainger in :gh:`128307`.)\n\n\nbdb\n---\n",
|
||||
"change_type": "modification",
|
||||
"lines_of_context": 6,
|
||||
"function_name": null,
|
||||
"class_name": null,
|
||||
"docstring": null,
|
||||
"coding_patterns": []
|
||||
}
|
||||
],
|
||||
"commit_message_style": "references_issue; has_body",
|
||||
"python_version": null,
|
||||
"pep_status": null
|
||||
},
|
||||
{
|
||||
"type": "commit",
|
||||
"repository": "cpython",
|
||||
"title": "Update CODEOWNERS (#126005)",
|
||||
"description": "Update CODEOWNERS (#126005)",
|
||||
"url": "https://github.com/python/cpython/commit/905eddceb2d61da9087f0d303aa7e4a405d2261a",
|
||||
"date": "2024-10-26T15:24:51Z",
|
||||
"sha_or_number": "905eddceb2d61da9087f0d303aa7e4a405d2261a",
|
||||
"files_changed": [
|
||||
".github/CODEOWNERS"
|
||||
],
|
||||
"additions": 2,
|
||||
"deletions": 2,
|
||||
"labels": [],
|
||||
"related_issues": [
|
||||
"126005"
|
||||
],
|
||||
"code_samples": [],
|
||||
"commit_message_style": "concise_subject; imperative_mood; references_issue",
|
||||
"python_version": null,
|
||||
"pep_status": null
|
||||
},
|
||||
{
|
||||
"type": "commit",
|
||||
"repository": "cpython",
|
||||
"title": "Withdraw most of my ownership in favor of Mark (#119611)",
|
||||
"description": "Withdraw most of my ownership in favor of Mark (#119611)",
|
||||
"url": "https://github.com/python/cpython/commit/3ff06ebec4e8b466f76078aa9c97cea2093d52ab",
|
||||
"date": "2024-05-27T18:07:16Z",
|
||||
"sha_or_number": "3ff06ebec4e8b466f76078aa9c97cea2093d52ab",
|
||||
"files_changed": [
|
||||
".github/CODEOWNERS"
|
||||
],
|
||||
"additions": 6,
|
||||
"deletions": 6,
|
||||
"labels": [],
|
||||
"related_issues": [
|
||||
"119611"
|
||||
],
|
||||
"code_samples": [],
|
||||
"commit_message_style": "references_issue",
|
||||
"python_version": null,
|
||||
"pep_status": null
|
||||
},
|
||||
{
|
||||
"type": "commit",
|
||||
"repository": "cpython",
|
||||
"title": "gh-117549: Don't use designated initializers in headers (#118580)",
|
||||
"description": "gh-117549: Don't use designated initializers in headers (#118580)\n\nThe designated initializer syntax in static inline functions in pycore_backoff.h\r\ncauses problems for C++ or MSVC users who aren't yet using C++20.\r\nWhile internal, pycore_backoff.h is included (indirectly, via pycore_code.h)\r\nby some key 3rd party software that does so for speed.",
|
||||
"url": "https://github.com/python/cpython/commit/40cc809902304f60c6e1c933191dd4d64e570e28",
|
||||
"date": "2024-05-05T19:28:55Z",
|
||||
"sha_or_number": "40cc809902304f60c6e1c933191dd4d64e570e28",
|
||||
"files_changed": [
|
||||
"Include/internal/pycore_backoff.h",
|
||||
"Misc/NEWS.d/next/Core and Builtins/2024-05-05-12-04-02.gh-issue-117549.kITawD.rst"
|
||||
],
|
||||
"additions": 12,
|
||||
"deletions": 2,
|
||||
"labels": [],
|
||||
"related_issues": [
|
||||
"118580"
|
||||
],
|
||||
"code_samples": [
|
||||
{
|
||||
"file_path": "Misc/NEWS.d/next/Core and Builtins/2024-05-05-12-04-02.gh-issue-117549.kITawD.rst",
|
||||
"language": "restructuredtext",
|
||||
"before_code": "",
|
||||
"after_code": "Don't use designated initializer syntax in inline functions in internal\nheaders. They cause problems for C++ or MSVC users who aren't yet using the\nlatest C++ standard (C++20). While internal, pycore_backoff.h, is included\n(indirectly, via pycore_code.h) by some key 3rd party software that does so\nfor speed.",
|
||||
"diff_context": "Don't use designated initializer syntax in inline functions in internal\nheaders. They cause problems for C++ or MSVC users who aren't yet using the\nlatest C++ standard (C++20). While internal, pycore_backoff.h, is included\n(indirectly, via pycore_code.h) by some key 3rd party software that does so\nfor speed.",
|
||||
"change_type": "addition",
|
||||
"lines_of_context": 0,
|
||||
"function_name": null,
|
||||
"class_name": null,
|
||||
"docstring": null,
|
||||
"coding_patterns": []
|
||||
}
|
||||
],
|
||||
"commit_message_style": "references_issue; has_body",
|
||||
"python_version": null,
|
||||
"pep_status": null
|
||||
},
|
||||
{
|
||||
"type": "commit",
|
||||
"repository": "cpython",
|
||||
"title": "gh-74929: Rudimentary docs for PEP 667 (#118581)",
|
||||
"description": "gh-74929: Rudimentary docs for PEP 667 (#118581)\n\nThis is *not* sufficient for the final 3.13 release, but it will do for beta 1:\r\n\r\n- What's new entry\r\n- Updated changelog entry (news blurb)\r\n- Mention the proxy for f_globals in the datamodel and Python frame object docs\r\n\r\nThis doesn't have any C API details (what's new refers to the PEP).",
|
||||
"url": "https://github.com/python/cpython/commit/9c13d9e37a194f574b8591da634bf98419786448",
|
||||
"date": "2024-05-05T15:31:26Z",
|
||||
"sha_or_number": "9c13d9e37a194f574b8591da634bf98419786448",
|
||||
"files_changed": [
|
||||
"Doc/c-api/frame.rst",
|
||||
"Doc/reference/datamodel.rst",
|
||||
"Doc/whatsnew/3.13.rst",
|
||||
"Misc/NEWS.d/next/Core and Builtins/2024-04-27-21-44-40.gh-issue-74929.C2nESp.rst"
|
||||
],
|
||||
"additions": 22,
|
||||
"deletions": 3,
|
||||
"labels": [],
|
||||
"related_issues": [
|
||||
"118581"
|
||||
],
|
||||
"code_samples": [
|
||||
{
|
||||
"file_path": "Doc/c-api/frame.rst",
|
||||
"language": "restructuredtext",
|
||||
"before_code": "\n.. c:function:: PyObject* PyFrame_GetLocals(PyFrameObject *frame)\n\n Get the *frame*'s :attr:`~frame.f_locals` attribute (:class:`dict`).\n\n Return a :term:`strong reference`.\n\n .. versionadded:: 3.11\n\n\n.. c:function:: int PyFrame_GetLineNumber(PyFrameObject *frame)\n",
|
||||
"after_code": "\n.. c:function:: PyObject* PyFrame_GetLocals(PyFrameObject *frame)\n\n Get the *frame*'s :attr:`~frame.f_locals` attribute.\n If the frame refers to a function or comprehension, this returns\n a write-through proxy object that allows modifying the locals.\n In all other cases (classes, modules) it returns the :class:`dict`\n representing the frame locals directly.\n\n Return a :term:`strong reference`.\n\n .. versionadded:: 3.11\n\n .. versionchanged:: 3.13\n Return a proxy object for functions and comprehensions.\n\n\n.. c:function:: int PyFrame_GetLineNumber(PyFrameObject *frame)\n",
|
||||
"diff_context": "\n.. c:function:: PyObject* PyFrame_GetLocals(PyFrameObject *frame)\n\n Get the *frame*'s :attr:`~frame.f_locals` attribute (:class:`dict`).\n Get the *frame*'s :attr:`~frame.f_locals` attribute.\n If the frame refers to a function or comprehension, this returns\n a write-through proxy object that allows modifying the locals.\n In all other cases (classes, modules) it returns the :class:`dict`\n representing the frame locals directly.\n\n Return a :term:`strong reference`.\n\n .. versionadded:: 3.11\n\n .. versionchanged:: 3.13\n Return a proxy object for functions and comprehensions.\n\n\n.. c:function:: int PyFrame_GetLineNumber(PyFrameObject *frame)\n",
|
||||
"change_type": "modification",
|
||||
"lines_of_context": 11,
|
||||
"function_name": null,
|
||||
"class_name": null,
|
||||
"docstring": null,
|
||||
"coding_patterns": []
|
||||
},
|
||||
{
|
||||
"file_path": "Doc/reference/datamodel.rst",
|
||||
"language": "restructuredtext",
|
||||
"before_code": "\n * - .. attribute:: frame.f_locals\n - The dictionary used by the frame to look up\n :ref:`local variables <naming>`\n\n * - .. attribute:: frame.f_globals\n - The dictionary used by the frame to look up",
|
||||
"after_code": "\n * - .. attribute:: frame.f_locals\n - The dictionary used by the frame to look up\n :ref:`local variables <naming>`.\n If the frame refers to a function or comprehension,\n this may return a write-through proxy object.\n\n .. versionchanged:: 3.13\n Return a proxy for functions and comprehensions.\n\n * - .. attribute:: frame.f_globals\n - The dictionary used by the frame to look up",
|
||||
"diff_context": "\n * - .. attribute:: frame.f_locals\n - The dictionary used by the frame to look up\n :ref:`local variables <naming>`\n :ref:`local variables <naming>`.\n If the frame refers to a function or comprehension,\n this may return a write-through proxy object.\n\n .. versionchanged:: 3.13\n Return a proxy for functions and comprehensions.\n\n * - .. attribute:: frame.f_globals\n - The dictionary used by the frame to look up",
|
||||
"change_type": "modification",
|
||||
"lines_of_context": 6,
|
||||
"function_name": null,
|
||||
"class_name": null,
|
||||
"docstring": null,
|
||||
"coding_patterns": []
|
||||
},
|
||||
{
|
||||
"file_path": "Doc/whatsnew/3.13.rst",
|
||||
"language": "restructuredtext",
|
||||
"before_code": " Performance improvements are modest -- we expect to be improving this\n over the next few releases.\n\nNew typing features:\n\n* :pep:`696`: Type parameters (:data:`typing.TypeVar`, :data:`typing.ParamSpec`,",
|
||||
"after_code": " Performance improvements are modest -- we expect to be improving this\n over the next few releases.\n\n* :pep:`667`: :attr:`FrameType.f_locals <frame.f_locals>` when used in\n a function now returns a write-through proxy to the frame's locals,\n rather than a ``dict``. See the PEP for corresponding C API changes\n and deprecations.\n\nNew typing features:\n\n* :pep:`696`: Type parameters (:data:`typing.TypeVar`, :data:`typing.ParamSpec`,",
|
||||
"diff_context": " Performance improvements are modest -- we expect to be improving this\n over the next few releases.\n\n* :pep:`667`: :attr:`FrameType.f_locals <frame.f_locals>` when used in\n a function now returns a write-through proxy to the frame's locals,\n rather than a ``dict``. See the PEP for corresponding C API changes\n and deprecations.\n\nNew typing features:\n\n* :pep:`696`: Type parameters (:data:`typing.TypeVar`, :data:`typing.ParamSpec`,",
|
||||
"change_type": "modification",
|
||||
"lines_of_context": 6,
|
||||
"function_name": null,
|
||||
"class_name": null,
|
||||
"docstring": null,
|
||||
"coding_patterns": []
|
||||
}
|
||||
],
|
||||
"commit_message_style": "concise_subject; references_issue; has_body",
|
||||
"python_version": null,
|
||||
"pep_status": null
|
||||
},
|
||||
{
|
||||
"type": "commit",
|
||||
"repository": "cpython",
|
||||
"title": "gh-118335: Rename --experimental-interpreter on Windows to --experimental-jit-interpreter (#118497)",
|
||||
"description": "gh-118335: Rename --experimental-interpreter on Windows to --experimental-jit-interpreter (#118497)\n\nAlso fix docs for this in whatsnew.",
|
||||
"url": "https://github.com/python/cpython/commit/a37b0932285b5e883b13a46ff2a32f15d7339894",
|
||||
"date": "2024-05-02T00:48:34Z",
|
||||
"sha_or_number": "a37b0932285b5e883b13a46ff2a32f15d7339894",
|
||||
"files_changed": [
|
||||
"Doc/whatsnew/3.13.rst",
|
||||
"PCbuild/build.bat"
|
||||
],
|
||||
"additions": 5,
|
||||
"deletions": 4,
|
||||
"labels": [],
|
||||
"related_issues": [
|
||||
"118497"
|
||||
],
|
||||
"code_samples": [],
|
||||
"commit_message_style": "references_issue; has_body",
|
||||
"python_version": null,
|
||||
"pep_status": null
|
||||
},
|
||||
{
|
||||
"type": "commit",
|
||||
"repository": "mypy",
|
||||
"title": "Support TypeGuard (PEP 647) (#9865)",
|
||||
"description": "Support TypeGuard (PEP 647) (#9865)\n\nPEP 647 is still in draft mode, but it is likely to be accepted, and this helps solve some real issues.",
|
||||
"url": "https://github.com/python/mypy/commit/fffbe88fc54807c8b10ac40456522ad2faf8d350",
|
||||
"date": "2021-01-18T18:13:36Z",
|
||||
"sha_or_number": "fffbe88fc54807c8b10ac40456522ad2faf8d350",
|
||||
"files_changed": [
|
||||
"mypy/checker.py",
|
||||
"mypy/checkexpr.py",
|
||||
"mypy/constraints.py",
|
||||
"mypy/expandtype.py",
|
||||
"mypy/fixup.py",
|
||||
"mypy/nodes.py",
|
||||
"mypy/test/testcheck.py",
|
||||
"mypy/typeanal.py",
|
||||
"mypy/types.py",
|
||||
"test-data/unit/check-python38.test",
|
||||
"test-data/unit/check-serialize.test",
|
||||
"test-data/unit/check-typeguard.test",
|
||||
"test-data/unit/lib-stub/typing_extensions.pyi"
|
||||
],
|
||||
"additions": 408,
|
||||
"deletions": 9,
|
||||
"labels": [],
|
||||
"related_issues": [
|
||||
"9865"
|
||||
],
|
||||
"code_samples": [
|
||||
{
|
||||
"file_path": "mypy/checker.py",
|
||||
"language": "python",
|
||||
"before_code": " if literal(expr) == LITERAL_TYPE:\n vartype = type_map[expr]\n return self.conditional_callable_type_map(expr, vartype)\n elif isinstance(node, ComparisonExpr):\n # Step 1: Obtain the types of each operand and whether or not we can\n # narrow their types. (For example, we shouldn't try narrowing the",
|
||||
"after_code": " if literal(expr) == LITERAL_TYPE:\n vartype = type_map[expr]\n return self.conditional_callable_type_map(expr, vartype)\n elif isinstance(node.callee, RefExpr):\n if node.callee.type_guard is not None:\n # TODO: Follow keyword args or *args, **kwargs\n if node.arg_kinds[0] != nodes.ARG_POS:\n self.fail(\"Type guard requires positional argument\", node)\n return {}, {}\n if literal(expr) == LITERAL_TYPE:\n return {expr: TypeGuardType(node.callee.type_guard)}, {}\n elif isinstance(node, ComparisonExpr):\n # Step 1: Obtain the types of each operand and whether or not we can\n # narrow their types. (For example, we shouldn't try narrowing the",
|
||||
"diff_context": " if literal(expr) == LITERAL_TYPE:\n vartype = type_map[expr]\n return self.conditional_callable_type_map(expr, vartype)\n elif isinstance(node.callee, RefExpr):\n if node.callee.type_guard is not None:\n # TODO: Follow keyword args or *args, **kwargs\n if node.arg_kinds[0] != nodes.ARG_POS:\n self.fail(\"Type guard requires positional argument\", node)\n return {}, {}\n if literal(expr) == LITERAL_TYPE:\n return {expr: TypeGuardType(node.callee.type_guard)}, {}\n elif isinstance(node, ComparisonExpr):\n # Step 1: Obtain the types of each operand and whether or not we can\n # narrow their types. (For example, we shouldn't try narrowing the",
|
||||
"change_type": "modification",
|
||||
"lines_of_context": 6,
|
||||
"function_name": null,
|
||||
"class_name": null,
|
||||
"docstring": null,
|
||||
"coding_patterns": []
|
||||
},
|
||||
{
|
||||
"file_path": "mypy/checkexpr.py",
|
||||
"language": "python",
|
||||
"before_code": " ret_type=self.object_type(),\n fallback=self.named_type('builtins.function'))\n callee_type = get_proper_type(self.accept(e.callee, type_context, always_allow_any=True))\n if (self.chk.options.disallow_untyped_calls and\n self.chk.in_checked_function() and\n isinstance(callee_type, CallableType)",
|
||||
"after_code": " ret_type=self.object_type(),\n fallback=self.named_type('builtins.function'))\n callee_type = get_proper_type(self.accept(e.callee, type_context, always_allow_any=True))\n if (isinstance(e.callee, RefExpr)\n and isinstance(callee_type, CallableType)\n and callee_type.type_guard is not None):\n # Cache it for find_isinstance_check()\n e.callee.type_guard = callee_type.type_guard\n if (self.chk.options.disallow_untyped_calls and\n self.chk.in_checked_function() and\n isinstance(callee_type, CallableType)",
|
||||
"diff_context": " ret_type=self.object_type(),\n fallback=self.named_type('builtins.function'))\n callee_type = get_proper_type(self.accept(e.callee, type_context, always_allow_any=True))\n if (isinstance(e.callee, RefExpr)\n and isinstance(callee_type, CallableType)\n and callee_type.type_guard is not None):\n # Cache it for find_isinstance_check()\n e.callee.type_guard = callee_type.type_guard\n if (self.chk.options.disallow_untyped_calls and\n self.chk.in_checked_function() and\n isinstance(callee_type, CallableType)",
|
||||
"change_type": "modification",
|
||||
"lines_of_context": 6,
|
||||
"function_name": null,
|
||||
"class_name": null,
|
||||
"docstring": null,
|
||||
"coding_patterns": [
|
||||
"generator_expression"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file_path": "mypy/checkexpr.py",
|
||||
"language": "python",
|
||||
"before_code": " \"\"\"\n if literal(expr) >= LITERAL_TYPE:\n restriction = self.chk.binder.get(expr)\n # If the current node is deferred, some variables may get Any types that they\n # otherwise wouldn't have. We don't want to narrow down these since it may\n # produce invalid inferred Optional[Any] types, at least.",
|
||||
"after_code": " \"\"\"\n if literal(expr) >= LITERAL_TYPE:\n restriction = self.chk.binder.get(expr)\n # Ignore the error about using get_proper_type().\n if isinstance(restriction, TypeGuardType): # type: ignore[misc]\n # A type guard forces the new type even if it doesn't overlap the old.\n return restriction.type_guard\n # If the current node is deferred, some variables may get Any types that they\n # otherwise wouldn't have. We don't want to narrow down these since it may\n # produce invalid inferred Optional[Any] types, at least.",
|
||||
"diff_context": " \"\"\"\n if literal(expr) >= LITERAL_TYPE:\n restriction = self.chk.binder.get(expr)\n # Ignore the error about using get_proper_type().\n if isinstance(restriction, TypeGuardType): # type: ignore[misc]\n # A type guard forces the new type even if it doesn't overlap the old.\n return restriction.type_guard\n # If the current node is deferred, some variables may get Any types that they\n # otherwise wouldn't have. We don't want to narrow down these since it may\n # produce invalid inferred Optional[Any] types, at least.",
|
||||
"change_type": "modification",
|
||||
"lines_of_context": 6,
|
||||
"function_name": null,
|
||||
"class_name": null,
|
||||
"docstring": "\"\"\"",
|
||||
"coding_patterns": [
|
||||
"type_hint"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file_path": "mypy/constraints.py",
|
||||
"language": "python",
|
||||
"before_code": " for t, a in zip(template.arg_types, cactual.arg_types):\n # Negate direction due to function argument type contravariance.\n res.extend(infer_constraints(t, a, neg_op(self.direction)))\n res.extend(infer_constraints(template.ret_type, cactual.ret_type,\n self.direction))\n return res\n elif isinstance(self.actual, AnyType):",
|
||||
"after_code": " for t, a in zip(template.arg_types, cactual.arg_types):\n # Negate direction due to function argument type contravariance.\n res.extend(infer_constraints(t, a, neg_op(self.direction)))\n template_ret_type, cactual_ret_type = template.ret_type, cactual.ret_type\n if template.type_guard is not None:\n template_ret_type = template.type_guard\n if cactual.type_guard is not None:\n cactual_ret_type = cactual.type_guard\n res.extend(infer_constraints(template_ret_type, cactual_ret_type,\n self.direction))\n return res\n elif isinstance(self.actual, AnyType):",
|
||||
"diff_context": " for t, a in zip(template.arg_types, cactual.arg_types):\n # Negate direction due to function argument type contravariance.\n res.extend(infer_constraints(t, a, neg_op(self.direction)))\n res.extend(infer_constraints(template.ret_type, cactual.ret_type,\n template_ret_type, cactual_ret_type = template.ret_type, cactual.ret_type\n if template.type_guard is not None:\n template_ret_type = template.type_guard\n if cactual.type_guard is not None:\n cactual_ret_type = cactual.type_guard\n res.extend(infer_constraints(template_ret_type, cactual_ret_type,\n self.direction))\n return res\n elif isinstance(self.actual, AnyType):",
|
||||
"change_type": "modification",
|
||||
"lines_of_context": 6,
|
||||
"function_name": null,
|
||||
"class_name": null,
|
||||
"docstring": null,
|
||||
"coding_patterns": [
|
||||
"type_hint"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file_path": "mypy/expandtype.py",
|
||||
"language": "python",
|
||||
"before_code": "\n def visit_callable_type(self, t: CallableType) -> Type:\n return t.copy_modified(arg_types=self.expand_types(t.arg_types),\n ret_type=t.ret_type.accept(self))\n\n def visit_overloaded(self, t: Overloaded) -> Type:\n items = [] # type: List[CallableType]",
|
||||
"after_code": "\n def visit_callable_type(self, t: CallableType) -> Type:\n return t.copy_modified(arg_types=self.expand_types(t.arg_types),\n ret_type=t.ret_type.accept(self),\n type_guard=(t.type_guard.accept(self)\n if t.type_guard is not None else None))\n\n def visit_overloaded(self, t: Overloaded) -> Type:\n items = [] # type: List[CallableType]",
|
||||
"diff_context": "\n def visit_callable_type(self, t: CallableType) -> Type:\n return t.copy_modified(arg_types=self.expand_types(t.arg_types),\n ret_type=t.ret_type.accept(self))\n ret_type=t.ret_type.accept(self),\n type_guard=(t.type_guard.accept(self)\n if t.type_guard is not None else None))\n\n def visit_overloaded(self, t: Overloaded) -> Type:\n items = [] # type: List[CallableType]",
|
||||
"change_type": "modification",
|
||||
"lines_of_context": 6,
|
||||
"function_name": "visit_overloaded",
|
||||
"class_name": null,
|
||||
"docstring": null,
|
||||
"coding_patterns": []
|
||||
},
|
||||
{
|
||||
"file_path": "mypy/fixup.py",
|
||||
"language": "python",
|
||||
"before_code": " for arg in ct.bound_args:\n if arg:\n arg.accept(self)\n\n def visit_overloaded(self, t: Overloaded) -> None:\n for ct in t.items():",
|
||||
"after_code": " for arg in ct.bound_args:\n if arg:\n arg.accept(self)\n if ct.type_guard is not None:\n ct.type_guard.accept(self)\n\n def visit_overloaded(self, t: Overloaded) -> None:\n for ct in t.items():",
|
||||
"diff_context": " for arg in ct.bound_args:\n if arg:\n arg.accept(self)\n if ct.type_guard is not None:\n ct.type_guard.accept(self)\n\n def visit_overloaded(self, t: Overloaded) -> None:\n for ct in t.items():",
|
||||
"change_type": "modification",
|
||||
"lines_of_context": 6,
|
||||
"function_name": "visit_overloaded",
|
||||
"class_name": null,
|
||||
"docstring": null,
|
||||
"coding_patterns": []
|
||||
}
|
||||
],
|
||||
"commit_message_style": "concise_subject; references_issue; has_body",
|
||||
"python_version": null,
|
||||
"pep_status": null
|
||||
},
|
||||
{
|
||||
"type": "commit",
|
||||
"repository": "mypy",
|
||||
"title": "Add a separate issue form to report crashes (#9549)",
|
||||
"description": "Add a separate issue form to report crashes (#9549)",
|
||||
"url": "https://github.com/python/mypy/commit/cca6e2fdc874b7538bd1d2ef70daab687b2a0363",
|
||||
"date": "2020-10-08T22:30:06Z",
|
||||
"sha_or_number": "cca6e2fdc874b7538bd1d2ef70daab687b2a0363",
|
||||
"files_changed": [
|
||||
".github/ISSUE_TEMPLATE/crash.md"
|
||||
],
|
||||
"additions": 41,
|
||||
"deletions": 0,
|
||||
"labels": [],
|
||||
"related_issues": [
|
||||
"9549"
|
||||
],
|
||||
"code_samples": [
|
||||
{
|
||||
"file_path": ".github/ISSUE_TEMPLATE/crash.md",
|
||||
"language": "markdown",
|
||||
"before_code": "",
|
||||
"after_code": "---\nname: Crash Report\nabout: Crash (traceback or \"INTERNAL ERROR\")\nlabels: \"crash\"\n---\n\n<!--\n Use this form only if mypy reports an \"INTERNAL ERROR\" and/or gives a traceback.\n Please include the traceback and all other messages below (use `mypy --show-traceback`).\n-->\n\n**Crash Report**\n\n(Tell us what happened.)\n\n**Traceback**\n\n```\n(Insert traceback and other messages from mypy here -- use `--show-traceback`.)\n```\n\n**To Reproduce**\n\n(Write what you did to reproduce the crash. Full source code is\nappreciated. We also very much appreciate it if you try to narrow the\nsource down to a small stand-alone example.)\n\n**Your Environment**\n\n<!-- Include as many relevant details about the environment you experienced the bug in -->\n\n- Mypy version used:\n- Mypy command-line flags:\n- Mypy configuration options from `mypy.ini` (and other config files):\n- Python version used:\n- Operating system and version:\n\n<!--\nYou can freely edit this text, please remove all the lines\nyou believe are unnecessary.\n-->",
|
||||
"diff_context": "---\nname: Crash Report\nabout: Crash (traceback or \"INTERNAL ERROR\")\nlabels: \"crash\"\n---\n\n<!--\n Use this form only if mypy reports an \"INTERNAL ERROR\" and/or gives a traceback.\n Please include the traceback and all other messages below (use `mypy --show-traceback`).\n-->\n\n**Crash Report**\n\n(Tell us what happened.)\n\n**Traceback**\n\n```\n(Insert traceback and other messages from mypy here -- use `--show-traceback`.)\n```\n\n**To Reproduce**\n\n(Write what you did to reproduce the crash. Full source code is\nappreciated. We also very much appreciate it if you try to narrow the\nsource down to a small stand-alone example.)\n\n**Your Environment**\n\n<!-- Include as many relevant details about the environment you experienced the bug in -->\n\n- Mypy version used:\n- Mypy command-line flags:\n- Mypy configuration options from `mypy.ini` (and other config files):\n- Python version used:\n- Operating system and version:\n\n<!--\nYou can freely edit this text, please remove all the lines\nyou believe are unnecessary.\n-->",
|
||||
"change_type": "addition",
|
||||
"lines_of_context": 0,
|
||||
"function_name": null,
|
||||
"class_name": null,
|
||||
"docstring": null,
|
||||
"coding_patterns": []
|
||||
}
|
||||
],
|
||||
"commit_message_style": "imperative_mood; references_issue",
|
||||
"python_version": null,
|
||||
"pep_status": null
|
||||
},
|
||||
{
|
||||
"type": "commit",
|
||||
"repository": "mypy",
|
||||
"title": "Make the new bug templates less markup-heavy (#9438)",
|
||||
"description": "Make the new bug templates less markup-heavy (#9438)\n\n- Remove emoji\r\n- Instead of `## H2 headings` just use `**bold**`\r\n- Add link to docs\r\n- Add suggestion for new users not to file a bug",
|
||||
"url": "https://github.com/python/mypy/commit/6f07cb6a2e02446b909846f99817f674675e826e",
|
||||
"date": "2020-09-11T18:35:59Z",
|
||||
"sha_or_number": "6f07cb6a2e02446b909846f99817f674675e826e",
|
||||
"files_changed": [
|
||||
".github/ISSUE_TEMPLATE/bug.md",
|
||||
".github/ISSUE_TEMPLATE/documentation.md",
|
||||
".github/ISSUE_TEMPLATE/feature.md",
|
||||
".github/ISSUE_TEMPLATE/question.md"
|
||||
],
|
||||
"additions": 24,
|
||||
"deletions": 18,
|
||||
"labels": [],
|
||||
"related_issues": [
|
||||
"9438"
|
||||
],
|
||||
"code_samples": [
|
||||
{
|
||||
"file_path": ".github/ISSUE_TEMPLATE/bug.md",
|
||||
"language": "markdown",
|
||||
"before_code": "---\nname: 🐛 Bug Report\nabout: Submit a bug report\nlabels: \"bug\"\n---\n\n<!--\nNote: If the problem you are reporting is about a specific library function, then the typeshed tracker is better suited\nfor this report: https://github.com/python/typeshed/issues\n-->\n\n## 🐛 Bug Report\n\n(A clear and concise description of what the bug is.)\n\n## To Reproduce\n\n(Write your steps here:)\n\n1. Step 1...\n1. Step 2...\n1. Step 3...\n\n## Expected Behavior\n\n<!--\n How did you expect your project to behave?",
|
||||
"after_code": "---\nname: Bug Report\nabout: Submit a bug report\nlabels: \"bug\"\n---\n\n<!--\n If you're new to mypy and you're not sure whether what you're experiencing is a mypy bug, please see the \"Question and Help\" form\n instead.\n-->\n\n**Bug Report**\n\n<!--\nNote: If the problem you are reporting is about a specific library function, then the typeshed tracker is better suited\nfor this report: https://github.com/python/typeshed/issues\n-->\n\n(A clear and concise description of what the bug is.)\n\n**To Reproduce**\n\n(Write your steps here:)\n\n1. Step 1...\n2. Step 2...\n3. Step 3...\n\n**Expected Behavior**\n\n<!--\n How did you expect your project to behave?",
|
||||
"diff_context": "---\nname: 🐛 Bug Report\nname: Bug Report\nabout: Submit a bug report\nlabels: \"bug\"\n---\n\n<!--\n If you're new to mypy and you're not sure whether what you're experiencing is a mypy bug, please see the \"Question and Help\" form\n instead.\n-->\n\n**Bug Report**\n\n<!--\nNote: If the problem you are reporting is about a specific library function, then the typeshed tracker is better suited\nfor this report: https://github.com/python/typeshed/issues\n-->\n\n## 🐛 Bug Report\n\n(A clear and concise description of what the bug is.)\n\n## To Reproduce\n**To Reproduce**\n\n(Write your steps here:)\n\n1. Step 1...\n1. Step 2...\n1. Step 3...\n2. Step 2...\n3. Step 3...\n\n## Expected Behavior\n**Expected Behavior**\n\n<!--\n How did you expect your project to behave?",
|
||||
"change_type": "modification",
|
||||
"lines_of_context": 20,
|
||||
"function_name": null,
|
||||
"class_name": null,
|
||||
"docstring": null,
|
||||
"coding_patterns": []
|
||||
}
|
||||
],
|
||||
"commit_message_style": "references_issue; has_body",
|
||||
"python_version": null,
|
||||
"pep_status": null
|
||||
},
|
||||
{
|
||||
"type": "commit",
|
||||
"repository": "mypy",
|
||||
"title": "Add MYPY_CONFIG_FILE_DIR to environment when config file is read (2nd try) (#9414)",
|
||||
"description": "Add MYPY_CONFIG_FILE_DIR to environment when config file is read (2nd try) (#9414)\n\n(This fixes the mistake I introduced in the previous version.)\r\n\r\nResubmit of #9403.\r\n\r\nFixes #7968.\r\n\r\nCo-authored-by: aghast <aghast@aghast.dev>",
|
||||
"url": "https://github.com/python/mypy/commit/9d038469d80e36057c77e0a8a18831f829778f9d",
|
||||
"date": "2020-09-04T20:55:14Z",
|
||||
"sha_or_number": "9d038469d80e36057c77e0a8a18831f829778f9d",
|
||||
"files_changed": [
|
||||
"mypy/config_parser.py",
|
||||
"mypy/test/testcmdline.py",
|
||||
"test-data/unit/envvars.test"
|
||||
],
|
||||
"additions": 15,
|
||||
"deletions": 0,
|
||||
"labels": [],
|
||||
"related_issues": [
|
||||
"9403",
|
||||
"7968",
|
||||
"9414"
|
||||
],
|
||||
"code_samples": [],
|
||||
"commit_message_style": "imperative_mood; references_issue; has_body",
|
||||
"python_version": null,
|
||||
"pep_status": null
|
||||
},
|
||||
{
|
||||
"type": "commit",
|
||||
"repository": "mypy",
|
||||
"title": "Revert \"Add MYPY_CONFIG_FILE_DIR to environment when config file is read (#9403)\"",
|
||||
"description": "Revert \"Add MYPY_CONFIG_FILE_DIR to environment when config file is read (#9403)\"\n\nReason: This broke CI.\n\nThis reverts commit 652aca96609c876c47ca7eaa68d67ac1e36f4215.",
|
||||
"url": "https://github.com/python/mypy/commit/57d3473ae906fe945953b874d3dcb66efb2710ca",
|
||||
"date": "2020-09-04T02:45:27Z",
|
||||
"sha_or_number": "57d3473ae906fe945953b874d3dcb66efb2710ca",
|
||||
"files_changed": [
|
||||
"mypy/config_parser.py",
|
||||
"mypy/test/testcmdline.py",
|
||||
"test-data/unit/envvars.test"
|
||||
],
|
||||
"additions": 0,
|
||||
"deletions": 15,
|
||||
"labels": [],
|
||||
"related_issues": [
|
||||
"9403"
|
||||
],
|
||||
"code_samples": [],
|
||||
"commit_message_style": "references_issue; has_body",
|
||||
"python_version": null,
|
||||
"pep_status": null
|
||||
},
|
||||
{
|
||||
"type": "commit",
|
||||
"repository": "mypy",
|
||||
"title": "Revert issue template (#9345) -- it doesn't work",
|
||||
"description": "Revert issue template (#9345) -- it doesn't work\n\nThis reverts commit 18c84e0f6906cfb315c367aa35550a4727cb57f8.",
|
||||
"url": "https://github.com/python/mypy/commit/42a522089c6b418727e143c181128e902acf0908",
|
||||
"date": "2020-08-27T22:21:28Z",
|
||||
"sha_or_number": "42a522089c6b418727e143c181128e902acf0908",
|
||||
"files_changed": [
|
||||
".github/ISSUE_TEMPLATE/bug.md",
|
||||
".github/ISSUE_TEMPLATE/documentation.md",
|
||||
".github/ISSUE_TEMPLATE/feature.md",
|
||||
".github/ISSUE_TEMPLATE/question.md",
|
||||
".github/PULL_REQUEST_TEMPLATE.md",
|
||||
"ISSUE_TEMPLATE.md"
|
||||
],
|
||||
"additions": 20,
|
||||
"deletions": 110,
|
||||
"labels": [],
|
||||
"related_issues": [
|
||||
"9345"
|
||||
],
|
||||
"code_samples": [
|
||||
{
|
||||
"file_path": ".github/ISSUE_TEMPLATE/bug.md",
|
||||
"language": "markdown",
|
||||
"before_code": "---\nname: 🐛 Bug Report\nlabels: \"bug\"\n---\n\n<!--\nNote: If the problem you are reporting is about a specific library function, then the typeshed tracker is better suited\nfor this report: https://github.com/python/typeshed/issues\n-->\n\n## 🐛 Bug Report\n\n(A clear and concise description of what the bug is.)\n\n## To Reproduce\n\n(Write your steps here:)\n\n1. Step 1...\n1. Step 2...\n1. Step 3...\n\n## Expected Behavior\n\n<!--\n How did you expect your project to behave?\n It’s fine if you’re not sure your understanding is correct.\n Write down what you thought would happen. If you just expected no errors, you can delete this section.\n-->\n\n(Write what you thought would happen.)\n\n## Actual Behavior\n\n<!--\n Did something go wrong?\n Is something broken, or not behaving as you expected?\n-->\n\n(Write what happened.)\n\n## Your Environment\n\n<!-- Include as many relevant details about the environment you experienced the bug in -->\n\n- Mypy version used:\n- Mypy command-line flags:\n- Mypy configuration options from `mypy.ini` (and other config files):\n- Python version used:\n- Operating system and version:\n\n<!--\nYou can freely edit this text, please remove all the lines\nyou believe are unnecessary.\n-->",
|
||||
"after_code": "",
|
||||
"diff_context": "---\nname: 🐛 Bug Report\nlabels: \"bug\"\n---\n\n<!--\nNote: If the problem you are reporting is about a specific library function, then the typeshed tracker is better suited\nfor this report: https://github.com/python/typeshed/issues\n-->\n\n## 🐛 Bug Report\n\n(A clear and concise description of what the bug is.)\n\n## To Reproduce\n\n(Write your steps here:)\n\n1. Step 1...\n1. Step 2...\n1. Step 3...\n\n## Expected Behavior\n\n<!--\n How did you expect your project to behave?\n It’s fine if you’re not sure your understanding is correct.\n Write down what you thought would happen. If you just expected no errors, you can delete this section.\n-->\n\n(Write what you thought would happen.)\n\n## Actual Behavior\n\n<!--\n Did something go wrong?\n Is something broken, or not behaving as you expected?\n-->\n\n(Write what happened.)\n\n## Your Environment\n\n<!-- Include as many relevant details about the environment you experienced the bug in -->\n\n- Mypy version used:\n- Mypy command-line flags:\n- Mypy configuration options from `mypy.ini` (and other config files):\n- Python version used:\n- Operating system and version:\n\n<!--\nYou can freely edit this text, please remove all the lines\nyou believe are unnecessary.\n-->",
|
||||
"change_type": "deletion",
|
||||
"lines_of_context": 0,
|
||||
"function_name": null,
|
||||
"class_name": null,
|
||||
"docstring": null,
|
||||
"coding_patterns": []
|
||||
},
|
||||
{
|
||||
"file_path": ".github/ISSUE_TEMPLATE/feature.md",
|
||||
"language": "markdown",
|
||||
"before_code": "---\nname: 🚀 Feature\nlabels: \"feature\"\n---\n\n## 🚀 Feature\n\n(A clear and concise description of your feature proposal.)\n\n## Pitch\n\n(Please explain why this feature should be implemented and how it would be used. Add examples, if applicable.)",
|
||||
"after_code": "",
|
||||
"diff_context": "---\nname: 🚀 Feature\nlabels: \"feature\"\n---\n\n## 🚀 Feature\n\n(A clear and concise description of your feature proposal.)\n\n## Pitch\n\n(Please explain why this feature should be implemented and how it would be used. Add examples, if applicable.)",
|
||||
"change_type": "deletion",
|
||||
"lines_of_context": 0,
|
||||
"function_name": null,
|
||||
"class_name": null,
|
||||
"docstring": null,
|
||||
"coding_patterns": []
|
||||
},
|
||||
{
|
||||
"file_path": ".github/PULL_REQUEST_TEMPLATE.md",
|
||||
"language": "markdown",
|
||||
"before_code": "### Have you read the [Contributing Guidelines](https://github.com/python/mypy/blob/master/CONTRIBUTING.md)?\n\n(Once you have, delete this section. If you leave it in, your PR may be closed without action.)\n\n### Description\n\n<!--\nIf this pull request closes or fixes an issue, write Closes #NNN\" or \"Fixes #NNN\" in that exact\nformat.\n-->\n\n(Explain how this PR changes mypy.)\n\n## Test Plan\n\n<!--\nIf this is a documentation change, rebuild the docs (link to instructions) and review the changed pages for markup errors.\nIf this is a code change, include new tests (link to the testing docs). Be sure to run the tests locally and fix any errors before submitting the PR (more instructions).\nIf this change cannot be tested by the CI, please explain how to verify it manually.\n-->\n\n(Write your test plan here. If you changed any code, please provide us with clear instructions on how you verified your changes work.)",
|
||||
"after_code": "",
|
||||
"diff_context": "### Have you read the [Contributing Guidelines](https://github.com/python/mypy/blob/master/CONTRIBUTING.md)?\n\n(Once you have, delete this section. If you leave it in, your PR may be closed without action.)\n\n### Description\n\n<!--\nIf this pull request closes or fixes an issue, write Closes #NNN\" or \"Fixes #NNN\" in that exact\nformat.\n-->\n\n(Explain how this PR changes mypy.)\n\n## Test Plan\n\n<!--\nIf this is a documentation change, rebuild the docs (link to instructions) and review the changed pages for markup errors.\nIf this is a code change, include new tests (link to the testing docs). Be sure to run the tests locally and fix any errors before submitting the PR (more instructions).\nIf this change cannot be tested by the CI, please explain how to verify it manually.\n-->\n\n(Write your test plan here. If you changed any code, please provide us with clear instructions on how you verified your changes work.)",
|
||||
"change_type": "deletion",
|
||||
"lines_of_context": 0,
|
||||
"function_name": null,
|
||||
"class_name": null,
|
||||
"docstring": null,
|
||||
"coding_patterns": []
|
||||
}
|
||||
],
|
||||
"commit_message_style": "concise_subject; references_issue; has_body",
|
||||
"python_version": null,
|
||||
"pep_status": null
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
Assistant Guidelines
|
||||
These rules are absolutely imperative to adhere to. Comply with them precisely as they are outlined.
|
||||
|
||||
The agent must use sequential thinking MCP tool to work out problems.
|
||||
|
||||
Core Behavior Guidelines
|
||||
|
||||
Respond only to explicit requests. Do not add files, code, tests, or comments unless asked.
|
||||
|
||||
Follow instructions precisely. No assumptions or speculative additions.
|
||||
|
||||
Use provided context accurately.
|
||||
|
||||
Avoid extra output. No debugging logs or test harnesses unless requested.
|
||||
|
||||
Produce clean, optimized code when code is requested. Respect existing style.
|
||||
|
||||
Deliver complete, standalone solutions. No placeholders.
|
||||
|
||||
Limit file creation. Only create new files when necessary.
|
||||
|
||||
If you modify the model in a user's code, you must confirm with the user and never be sneaky. Always tell the user exactly what you are doing.
|
||||
|
||||
Communication & Delivery
|
||||
|
||||
9. Don't explain unless asked. Do not expose reasoning in outputs.
|
||||
10. If unsure, say "I don't know." Avoid hallucinated content.
|
||||
11. Maintain consistency across sessions. Refer to project memory and documentation.
|
||||
12. Respect privacy and permissions. Never leak or infer secure data.
|
||||
13. Prioritize targeted edits over full rewrites.
|
||||
14. Optimize incrementally. Avoid unnecessary overhauls.
|
||||
|
||||
Spec.md Requirement
|
||||
|
||||
You must maintain a file named Spec.md. This file acts as the single source of truth for the project.
|
||||
|
||||
Rules:
|
||||
|
||||
Before starting any implementation, check if Spec.md already exists.
|
||||
|
||||
If it does not exist, create one using the template provided below.
|
||||
|
||||
Always update Spec.md before and after any major change.
|
||||
|
||||
Use the contents of Spec.md to guide logic, structure, and implementation decisions.
|
||||
|
||||
When updating a section, condense previous content to keep the document concise.
|
||||
|
||||
Spec.md Starter Template (Plain Text Format)
|
||||
|
||||
Title: Spec.md – Project Specification
|
||||
|
||||
Section: Purpose
|
||||
Describe the main goal of this feature, tool, or system.
|
||||
|
||||
Section: Core Functionality
|
||||
List the key features, expected behaviors, and common use cases.
|
||||
|
||||
Section: Architecture Overview
|
||||
Summarize the technical setup, frameworks used, and main modules or services.
|
||||
|
||||
Section: Input and Output Contracts
|
||||
List all inputs and outputs in a table-like format:
|
||||
|
||||
Input: describe the input data, its format, and where it comes from.
|
||||
|
||||
Output: describe the output data, its format, and its destination.
|
||||
|
||||
Section: Edge Cases and Constraints
|
||||
List known limitations, special scenarios, and fallback behaviors.
|
||||
|
||||
Section: File and Module Map
|
||||
List all important files or modules and describe what each one is responsible for.
|
||||
|
||||
Section: Open Questions or TODOs
|
||||
Create a checklist of unresolved decisions, logic that needs clarification, or tasks that are still pending.
|
||||
|
||||
Section: Last Updated
|
||||
Include the most recent update date and who made the update.
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
# PEP 8 Style Guide: Essentials
|
||||
|
||||
## Code Layout
|
||||
- Indentation: 4 spaces per level
|
||||
- Line length: 79 for code (88/100 acceptable by team), 72 for comments/docstrings
|
||||
- Blank lines: 2 around top-level defs/classes, 1 between methods
|
||||
|
||||
```python
|
||||
# Hanging indent for long calls
|
||||
foo = long_function_name(
|
||||
var_one, var_two,
|
||||
var_three, var_four,
|
||||
)
|
||||
```
|
||||
|
||||
## Imports
|
||||
- One import per line
|
||||
- Group: stdlib, third-party, local
|
||||
- Prefer absolute imports; avoid wildcard imports
|
||||
|
||||
```python
|
||||
import os
|
||||
import sys
|
||||
from subprocess import Popen, PIPE
|
||||
|
||||
import requests
|
||||
|
||||
from myproject.models import User
|
||||
```
|
||||
|
||||
## Whitespace
|
||||
- No space inside brackets or before commas/semicolons
|
||||
- Spaces around binary operators
|
||||
|
||||
```python
|
||||
x = 1
|
||||
hypot2 = x * x + y * y
|
||||
```
|
||||
|
||||
## Naming
|
||||
- snake_case: functions, variables
|
||||
- PascalCase: classes
|
||||
- SCREAMING_SNAKE_CASE: constants
|
||||
|
||||
## Comments & Docstrings
|
||||
- Use complete sentences; keep up to date
|
||||
- Triple-double quotes for public modules, classes, functions
|
||||
```python
|
||||
def f(x: int) -> int:
|
||||
"""Return x doubled."""
|
||||
return x * 2
|
||||
```
|
||||
|
||||
## Type Hints
|
||||
- Space after colon; arrow for returns
|
||||
```python
|
||||
def munge(s: str) -> str: ...
|
||||
```
|
||||
|
||||
## Tooling
|
||||
- Black, isort, Flake8 (or Ruff) to automate style
|
||||
- Example pyproject.toml excerpt:
|
||||
```toml
|
||||
[tool.black]
|
||||
line-length = 88
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
```
|
||||
|
||||
## Common Violations
|
||||
- E501: line too long -> break with parentheses
|
||||
- E225: missing whitespace around operator
|
||||
- E402: module import not at top of file
|
||||
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
# The Zen of Python: Practical Guide
|
||||
|
||||
## Overview
|
||||
The Zen of Python (Tim Peters, import this) captures Python's philosophy. Use these principles as a checklist during design, coding, and reviews.
|
||||
|
||||
## Key Principles With Guidance
|
||||
|
||||
### 1. Beautiful is better than ugly
|
||||
Prefer descriptive names, clear structure, and consistent formatting.
|
||||
|
||||
### 2. Explicit is better than implicit
|
||||
Be clear about behavior, imports, and types.
|
||||
```python
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
def get_future_date(days_ahead: int) -> datetime:
|
||||
return datetime.now() + timedelta(days=days_ahead)
|
||||
```
|
||||
|
||||
### 3. Simple is better than complex
|
||||
Choose straightforward solutions first.
|
||||
|
||||
### 4. Complex is better than complicated
|
||||
When complexity is needed, organize it with clear abstractions.
|
||||
|
||||
### 5. Flat is better than nested
|
||||
Use early returns to reduce indentation.
|
||||
|
||||
### 6. Sparse is better than dense
|
||||
Give code room to breathe with whitespace.
|
||||
|
||||
### 7. Readability counts
|
||||
Optimize for human readers; add docstrings for nontrivial code.
|
||||
|
||||
### 8. Special cases aren't special enough to break the rules
|
||||
Stay consistent; exceptions should be rare and justified.
|
||||
|
||||
### 9. Although practicality beats purity
|
||||
Prefer practical solutions that teams can maintain.
|
||||
|
||||
### 10. Errors should never pass silently
|
||||
Handle exceptions explicitly; log with context.
|
||||
|
||||
### 11. Unless explicitly silenced
|
||||
Silence only specific, acceptable errors and document why.
|
||||
|
||||
### 12. In the face of ambiguity, refuse the temptation to guess
|
||||
Require explicit inputs and behavior.
|
||||
|
||||
### 13. There should be one obvious way to do it
|
||||
Prefer standard library patterns and idioms.
|
||||
|
||||
### 14. Although that way may not be obvious at first
|
||||
Learn Python idioms; embrace clarity over novelty.
|
||||
|
||||
### 15. Now is better than never; 16. Never is often better than right now
|
||||
Iterate, but don't rush broken code.
|
||||
|
||||
### 17/18. Hard to explain is bad; easy to explain is good
|
||||
Prefer designs you can explain simply.
|
||||
|
||||
### 19. Namespaces are one honking great idea
|
||||
Use modules/packages to separate concerns; avoid wildcard imports.
|
||||
|
||||
## Modern Python Tie-ins
|
||||
- Type hints reinforce explicitness
|
||||
- Context managers enforce safe resource handling
|
||||
- Dataclasses improve readability for data containers
|
||||
|
||||
## Quick Review Checklist
|
||||
- Is it readable and explicit?
|
||||
- Is this the simplest working solution?
|
||||
- Are errors explicit and logged?
|
||||
- Are modules/namespaces used appropriately?
|
||||
|
||||
|
|
@ -1,18 +1,9 @@
|
|||
from uuid import UUID, uuid4
|
||||
from fastapi_users.exceptions import UserAlreadyExists
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from cognee.infrastructure.databases.relational import get_relational_engine
|
||||
from cognee.modules.notebooks.models.Notebook import Notebook
|
||||
from cognee.modules.notebooks.methods.create_notebook import _create_tutorial_notebook
|
||||
from cognee.modules.users.exceptions import TenantNotFoundError
|
||||
from cognee.modules.users.get_user_manager import get_user_manager_context
|
||||
from cognee.modules.users.get_user_db import get_user_db_context
|
||||
from cognee.modules.users.models.User import UserCreate
|
||||
from cognee.modules.users.models.Tenant import Tenant
|
||||
|
||||
from sqlalchemy import select
|
||||
from typing import Optional
|
||||
|
||||
|
||||
async def create_user(
|
||||
|
|
|
|||
|
|
@ -1,19 +1,29 @@
|
|||
import json
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, patch, MagicMock
|
||||
import hashlib
|
||||
import time
|
||||
from uuid import uuid4
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from uuid import uuid4, uuid5, NAMESPACE_OID
|
||||
from pathlib import Path
|
||||
import zipfile
|
||||
from cognee.shared.cache import get_tutorial_data_dir
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
import tempfile
|
||||
import shutil
|
||||
import importlib
|
||||
|
||||
from cognee.modules.notebooks.methods.create_notebook import _create_tutorial_notebook
|
||||
from cognee.modules.notebooks.models.Notebook import Notebook
|
||||
import cognee
|
||||
from cognee.modules.notebooks.methods.create_tutorial_notebooks import (
|
||||
create_tutorial_notebooks,
|
||||
_parse_cell_index,
|
||||
_get_cell_type,
|
||||
_extract_markdown_heading,
|
||||
_get_cell_name,
|
||||
_format_tutorial_name,
|
||||
_load_tutorial_cells,
|
||||
)
|
||||
|
||||
from cognee.modules.notebooks.models.Notebook import Notebook, NotebookCell
|
||||
from cognee.shared.logging_utils import get_logger
|
||||
|
||||
create_tutorial_notebooks_module = importlib.import_module(
|
||||
"cognee.modules.notebooks.methods.create_tutorial_notebooks"
|
||||
)
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
|
||||
|
|
@ -28,55 +38,558 @@ def mock_session():
|
|||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_jupyter_notebook():
|
||||
"""Sample Jupyter notebook content for testing."""
|
||||
return {
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": ["# Tutorial Introduction\n", "\n", "This is a tutorial notebook."],
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": None,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["import cognee\n", "print('Hello, Cognee!')"],
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": ["## Step 1: Data Ingestion\n", "\n", "Let's add some data."],
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": None,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["# Add your data here\n", "# await cognee.add('data.txt')"],
|
||||
},
|
||||
{
|
||||
"cell_type": "raw",
|
||||
"metadata": {},
|
||||
"source": ["This is a raw cell that should be skipped"],
|
||||
},
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4,
|
||||
}
|
||||
def temp_tutorials_dir():
|
||||
"""Create a temporary tutorials directory for testing."""
|
||||
temp_dir = Path(tempfile.mkdtemp())
|
||||
tutorials_dir = temp_dir / "tutorials"
|
||||
tutorials_dir.mkdir(parents=True)
|
||||
yield tutorials_dir
|
||||
shutil.rmtree(temp_dir)
|
||||
|
||||
|
||||
class TestTutorialNotebookCreation:
|
||||
"""Test cases for tutorial notebook creation functionality."""
|
||||
class TestTutorialNotebookHelperFunctions:
|
||||
"""Test cases for helper functions used in tutorial notebook creation."""
|
||||
|
||||
def test_parse_cell_index_valid(self):
|
||||
"""Test parsing valid cell index from filename."""
|
||||
assert _parse_cell_index("cell-0.md") == 0
|
||||
assert _parse_cell_index("cell-1.py") == 1
|
||||
assert _parse_cell_index("cell-123.md") == 123
|
||||
assert _parse_cell_index("cell-999.py") == 999
|
||||
|
||||
def test_parse_cell_index_invalid(self):
|
||||
"""Test parsing invalid cell index returns -1."""
|
||||
assert _parse_cell_index("not-a-cell.md") == -1
|
||||
assert _parse_cell_index("cell.md") == -1
|
||||
assert _parse_cell_index("cell-.md") == -1
|
||||
assert _parse_cell_index("") == -1
|
||||
|
||||
def test_get_cell_type_markdown(self):
|
||||
"""Test cell type detection for markdown files."""
|
||||
assert _get_cell_type(Path("cell-1.md")) == "markdown"
|
||||
assert _get_cell_type(Path("test.MD")) == "markdown"
|
||||
|
||||
def test_get_cell_type_code(self):
|
||||
"""Test cell type detection for Python files."""
|
||||
assert _get_cell_type(Path("cell-1.py")) == "code"
|
||||
assert _get_cell_type(Path("test.PY")) == "code"
|
||||
|
||||
def test_get_cell_type_unsupported(self):
|
||||
"""Test error handling for unsupported file types."""
|
||||
with pytest.raises(ValueError, match="Unsupported cell file type"):
|
||||
_get_cell_type(Path("cell-1.txt"))
|
||||
|
||||
def test_extract_markdown_heading_single_hash(self):
|
||||
"""Test extracting heading from markdown with single #."""
|
||||
content = "# My Heading\nSome content here"
|
||||
assert _extract_markdown_heading(content) == "My Heading"
|
||||
|
||||
def test_extract_markdown_heading_multiple_hash(self):
|
||||
"""Test extracting heading from markdown with multiple #."""
|
||||
content = "## Subheading\nSome content"
|
||||
assert _extract_markdown_heading(content) == "Subheading"
|
||||
|
||||
def test_extract_markdown_heading_with_whitespace(self):
|
||||
"""Test extracting heading with leading/trailing whitespace."""
|
||||
content = " # Heading with spaces \nContent"
|
||||
assert _extract_markdown_heading(content) == "Heading with spaces"
|
||||
|
||||
def test_extract_markdown_heading_no_heading(self):
|
||||
"""Test extracting heading when no heading exists."""
|
||||
content = "Just some regular text\nNo heading here"
|
||||
assert _extract_markdown_heading(content) is None
|
||||
|
||||
def test_extract_markdown_heading_empty(self):
|
||||
"""Test extracting heading from empty content."""
|
||||
assert _extract_markdown_heading("") is None
|
||||
|
||||
def test_get_cell_name_code_cell(self):
|
||||
"""Test cell name generation for code cells."""
|
||||
cell_file = Path("cell-1.py")
|
||||
content = "print('Hello, World!')"
|
||||
assert _get_cell_name(cell_file, "code", content) == "Code Cell"
|
||||
|
||||
def test_get_cell_name_markdown_with_heading(self):
|
||||
"""Test cell name generation for markdown cells with heading."""
|
||||
cell_file = Path("cell-1.md")
|
||||
content = "# My Tutorial Title\nSome content"
|
||||
assert _get_cell_name(cell_file, "markdown", content) == "My Tutorial Title"
|
||||
|
||||
def test_get_cell_name_markdown_no_heading(self):
|
||||
"""Test cell name generation for markdown cells without heading."""
|
||||
cell_file = Path("cell-1.md")
|
||||
content = "Just some text without heading"
|
||||
assert _get_cell_name(cell_file, "markdown", content) == "cell-1"
|
||||
|
||||
def test_format_tutorial_name_simple(self):
|
||||
"""Test formatting simple tutorial directory name."""
|
||||
assert _format_tutorial_name("cognee-basics") == "Cognee basics - tutorial 🧠"
|
||||
|
||||
def test_format_tutorial_name_with_underscores(self):
|
||||
"""Test formatting tutorial name with underscores."""
|
||||
assert _format_tutorial_name("python_development") == "Python development - tutorial 🧠"
|
||||
|
||||
def test_format_tutorial_name_mixed(self):
|
||||
"""Test formatting tutorial name with mixed separators."""
|
||||
assert _format_tutorial_name("my-tutorial_name") == "My tutorial name - tutorial 🧠"
|
||||
|
||||
|
||||
class TestLoadTutorialCells:
|
||||
"""Test cases for loading cells from tutorial directories."""
|
||||
|
||||
def test_load_tutorial_cells_sorted_order(self, temp_tutorials_dir):
|
||||
"""Test that cells are loaded in sorted order by index."""
|
||||
# Create cells out of order
|
||||
(temp_tutorials_dir / "cell-3.md").write_text("# Third")
|
||||
(temp_tutorials_dir / "cell-1.md").write_text("# First")
|
||||
(temp_tutorials_dir / "cell-2.py").write_text("print('second')")
|
||||
|
||||
cells = _load_tutorial_cells(temp_tutorials_dir)
|
||||
|
||||
assert len(cells) == 3
|
||||
assert cells[0].name == "First"
|
||||
assert cells[1].name == "Code Cell"
|
||||
assert cells[2].name == "Third"
|
||||
|
||||
def test_load_tutorial_cells_skips_non_cell_files(self, temp_tutorials_dir):
|
||||
"""Test that non-cell files are skipped."""
|
||||
(temp_tutorials_dir / "cell-1.md").write_text("# First")
|
||||
(temp_tutorials_dir / "config.json").write_text('{"name": "test"}')
|
||||
(temp_tutorials_dir / "README.md").write_text("# Readme")
|
||||
(temp_tutorials_dir / "data").mkdir()
|
||||
(temp_tutorials_dir / "data" / "file.txt").write_text("data")
|
||||
|
||||
cells = _load_tutorial_cells(temp_tutorials_dir)
|
||||
|
||||
assert len(cells) == 1
|
||||
assert cells[0].name == "First"
|
||||
|
||||
def test_load_tutorial_cells_skips_unsupported_extensions(self, temp_tutorials_dir):
|
||||
"""Test that unsupported file extensions are skipped."""
|
||||
(temp_tutorials_dir / "cell-1.md").write_text("# First")
|
||||
(temp_tutorials_dir / "cell-2.txt").write_text("Text file")
|
||||
(temp_tutorials_dir / "cell-3.py").write_text("print('code')")
|
||||
|
||||
cells = _load_tutorial_cells(temp_tutorials_dir)
|
||||
|
||||
assert len(cells) == 2
|
||||
assert cells[0].name == "First"
|
||||
assert cells[1].name == "Code Cell"
|
||||
|
||||
def test_load_tutorial_cells_empty_directory(self, temp_tutorials_dir):
|
||||
"""Test loading cells from empty directory."""
|
||||
cells = _load_tutorial_cells(temp_tutorials_dir)
|
||||
assert len(cells) == 0
|
||||
|
||||
def test_load_tutorial_cells_preserves_content(self, temp_tutorials_dir):
|
||||
"""Test that cell content is preserved correctly."""
|
||||
markdown_content = "# My Heading\n\nSome content here."
|
||||
code_content = "import cognee\nprint('Hello')"
|
||||
|
||||
(temp_tutorials_dir / "cell-1.md").write_text(markdown_content)
|
||||
(temp_tutorials_dir / "cell-2.py").write_text(code_content)
|
||||
|
||||
cells = _load_tutorial_cells(temp_tutorials_dir)
|
||||
|
||||
assert len(cells) == 2
|
||||
assert cells[0].content == markdown_content
|
||||
assert cells[0].type == "markdown"
|
||||
assert cells[1].content == code_content
|
||||
assert cells[1].type == "code"
|
||||
|
||||
|
||||
class TestCreateTutorialNotebooks:
|
||||
"""Test cases for the main create_tutorial_notebooks function."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_notebook_from_ipynb_string_success(self, sample_jupyter_notebook):
|
||||
async def test_create_tutorial_notebooks_success_with_config(
|
||||
self, mock_session, temp_tutorials_dir
|
||||
):
|
||||
"""Test successful creation of tutorial notebooks with config.json."""
|
||||
import json
|
||||
|
||||
user_id = uuid4()
|
||||
|
||||
# Create a tutorial directory with cells and config.json
|
||||
tutorial_dir = temp_tutorials_dir / "test-tutorial"
|
||||
tutorial_dir.mkdir()
|
||||
(tutorial_dir / "cell-1.md").write_text("# Introduction\nWelcome to the tutorial")
|
||||
(tutorial_dir / "cell-2.py").write_text("print('Hello')")
|
||||
(tutorial_dir / "config.json").write_text(
|
||||
json.dumps({"name": "Custom Tutorial Name", "deletable": False})
|
||||
)
|
||||
|
||||
with patch.object(
|
||||
create_tutorial_notebooks_module, "_get_tutorials_directory"
|
||||
) as mock_get_dir:
|
||||
mock_get_dir.return_value = temp_tutorials_dir
|
||||
|
||||
await create_tutorial_notebooks(user_id, mock_session)
|
||||
|
||||
# Verify notebook was added to session
|
||||
assert mock_session.add.call_count == 1
|
||||
added_notebook = mock_session.add.call_args[0][0]
|
||||
|
||||
assert isinstance(added_notebook, Notebook)
|
||||
assert added_notebook.owner_id == user_id
|
||||
assert added_notebook.name == "Custom Tutorial Name"
|
||||
assert len(added_notebook.cells) == 2
|
||||
assert added_notebook.deletable is False
|
||||
|
||||
# Verify deterministic ID generation based on config name
|
||||
expected_id = uuid5(NAMESPACE_OID, name="Custom Tutorial Name")
|
||||
assert added_notebook.id == expected_id
|
||||
|
||||
# Verify commit was called
|
||||
mock_session.commit.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_tutorial_notebooks_success_without_config(
|
||||
self, mock_session, temp_tutorials_dir
|
||||
):
|
||||
"""Test successful creation of tutorial notebooks without config.json (fallback)."""
|
||||
user_id = uuid4()
|
||||
|
||||
# Create a tutorial directory with cells but no config.json
|
||||
tutorial_dir = temp_tutorials_dir / "test-tutorial"
|
||||
tutorial_dir.mkdir()
|
||||
(tutorial_dir / "cell-1.md").write_text("# Introduction\nWelcome to the tutorial")
|
||||
(tutorial_dir / "cell-2.py").write_text("print('Hello')")
|
||||
|
||||
with patch.object(
|
||||
create_tutorial_notebooks_module, "_get_tutorials_directory"
|
||||
) as mock_get_dir:
|
||||
mock_get_dir.return_value = temp_tutorials_dir
|
||||
|
||||
await create_tutorial_notebooks(user_id, mock_session)
|
||||
|
||||
# Verify notebook was added to session
|
||||
assert mock_session.add.call_count == 1
|
||||
added_notebook = mock_session.add.call_args[0][0]
|
||||
|
||||
assert isinstance(added_notebook, Notebook)
|
||||
assert added_notebook.owner_id == user_id
|
||||
assert added_notebook.name == "Test tutorial - tutorial 🧠"
|
||||
assert len(added_notebook.cells) == 2
|
||||
assert added_notebook.deletable is False # Default for tutorials
|
||||
|
||||
# Verify deterministic ID generation
|
||||
expected_id = uuid5(NAMESPACE_OID, name="Test tutorial - tutorial 🧠")
|
||||
assert added_notebook.id == expected_id
|
||||
|
||||
# Verify commit was called
|
||||
mock_session.commit.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_tutorial_notebooks_multiple_tutorials(
|
||||
self, mock_session, temp_tutorials_dir
|
||||
):
|
||||
"""Test creation of multiple tutorial notebooks."""
|
||||
user_id = uuid4()
|
||||
|
||||
# Create two tutorial directories
|
||||
tutorial1 = temp_tutorials_dir / "tutorial-one"
|
||||
tutorial1.mkdir()
|
||||
(tutorial1 / "cell-1.md").write_text("# Tutorial One")
|
||||
|
||||
tutorial2 = temp_tutorials_dir / "tutorial-two"
|
||||
tutorial2.mkdir()
|
||||
(tutorial2 / "cell-1.md").write_text("# Tutorial Two")
|
||||
|
||||
with patch.object(
|
||||
create_tutorial_notebooks_module, "_get_tutorials_directory"
|
||||
) as mock_get_dir:
|
||||
mock_get_dir.return_value = temp_tutorials_dir
|
||||
|
||||
await create_tutorial_notebooks(user_id, mock_session)
|
||||
|
||||
# Verify both notebooks were added
|
||||
assert mock_session.add.call_count == 2
|
||||
mock_session.commit.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_tutorial_notebooks_skips_empty_tutorials(
|
||||
self, mock_session, temp_tutorials_dir
|
||||
):
|
||||
"""Test that tutorials with no cells are skipped."""
|
||||
user_id = uuid4()
|
||||
|
||||
# Create tutorial directory without cells
|
||||
tutorial_dir = temp_tutorials_dir / "empty-tutorial"
|
||||
tutorial_dir.mkdir()
|
||||
|
||||
with patch.object(
|
||||
create_tutorial_notebooks_module, "_get_tutorials_directory"
|
||||
) as mock_get_dir:
|
||||
mock_get_dir.return_value = temp_tutorials_dir
|
||||
|
||||
await create_tutorial_notebooks(user_id, mock_session)
|
||||
|
||||
# Verify no notebooks were added
|
||||
mock_session.add.assert_not_called()
|
||||
mock_session.commit.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_tutorial_notebooks_skips_hidden_directories(
|
||||
self, mock_session, temp_tutorials_dir
|
||||
):
|
||||
"""Test that hidden directories (starting with .) are skipped."""
|
||||
user_id = uuid4()
|
||||
|
||||
# Create hidden tutorial directory
|
||||
hidden_tutorial = temp_tutorials_dir / ".hidden-tutorial"
|
||||
hidden_tutorial.mkdir()
|
||||
(hidden_tutorial / "cell-1.md").write_text("# Hidden")
|
||||
|
||||
# Create visible tutorial directory
|
||||
visible_tutorial = temp_tutorials_dir / "visible-tutorial"
|
||||
visible_tutorial.mkdir()
|
||||
(visible_tutorial / "cell-1.md").write_text("# Visible")
|
||||
|
||||
with patch.object(
|
||||
create_tutorial_notebooks_module, "_get_tutorials_directory"
|
||||
) as mock_get_dir:
|
||||
mock_get_dir.return_value = temp_tutorials_dir
|
||||
|
||||
await create_tutorial_notebooks(user_id, mock_session)
|
||||
|
||||
# Verify only visible tutorial was added
|
||||
assert mock_session.add.call_count == 1
|
||||
added_notebook = mock_session.add.call_args[0][0]
|
||||
assert added_notebook.name == "Visible tutorial - tutorial 🧠"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_tutorial_notebooks_missing_directory(self, mock_session):
|
||||
"""Test handling when tutorials directory doesn't exist."""
|
||||
user_id = uuid4()
|
||||
|
||||
with patch.object(
|
||||
create_tutorial_notebooks_module, "_get_tutorials_directory"
|
||||
) as mock_get_dir:
|
||||
mock_get_dir.return_value = Path("/nonexistent/tutorials/dir")
|
||||
|
||||
await create_tutorial_notebooks(user_id, mock_session)
|
||||
|
||||
# Verify no notebooks were added and no commit
|
||||
mock_session.add.assert_not_called()
|
||||
mock_session.commit.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_tutorial_notebooks_empty_directory(
|
||||
self, mock_session, temp_tutorials_dir
|
||||
):
|
||||
"""Test handling when tutorials directory is empty."""
|
||||
user_id = uuid4()
|
||||
|
||||
with patch.object(
|
||||
create_tutorial_notebooks_module, "_get_tutorials_directory"
|
||||
) as mock_get_dir:
|
||||
mock_get_dir.return_value = temp_tutorials_dir
|
||||
|
||||
await create_tutorial_notebooks(user_id, mock_session)
|
||||
|
||||
# Verify no notebooks were added
|
||||
mock_session.add.assert_not_called()
|
||||
mock_session.commit.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_tutorial_notebooks_handles_cell_loading_error(
|
||||
self, mock_session, temp_tutorials_dir
|
||||
):
|
||||
"""Test that errors loading individual cells don't stop notebook creation."""
|
||||
user_id = uuid4()
|
||||
|
||||
tutorial_dir = temp_tutorials_dir / "test-tutorial"
|
||||
tutorial_dir.mkdir()
|
||||
(tutorial_dir / "cell-1.md").write_text("# Valid Cell")
|
||||
# Create a file that will cause an error (invalid extension that passes filter)
|
||||
invalid_file = tutorial_dir / "cell-2.invalid"
|
||||
invalid_file.write_text("Invalid content")
|
||||
|
||||
with patch.object(create_tutorial_notebooks_module, "_load_tutorial_cells") as mock_load:
|
||||
# Simulate error loading one cell but others succeed
|
||||
mock_load.return_value = [
|
||||
NotebookCell(id=uuid4(), type="markdown", name="Valid Cell", content="# Valid Cell")
|
||||
]
|
||||
|
||||
with patch.object(
|
||||
create_tutorial_notebooks_module, "_get_tutorials_directory"
|
||||
) as mock_get_dir:
|
||||
mock_get_dir.return_value = temp_tutorials_dir
|
||||
|
||||
await create_tutorial_notebooks(user_id, mock_session)
|
||||
|
||||
# Verify notebook was still created with valid cells
|
||||
assert mock_session.add.call_count == 1
|
||||
mock_session.commit.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_tutorial_notebooks_handles_tutorial_error_gracefully(
|
||||
self, mock_session, temp_tutorials_dir
|
||||
):
|
||||
"""Test that errors in one tutorial don't prevent others from being created."""
|
||||
user_id = uuid4()
|
||||
|
||||
# Create two tutorials - one will fail, one will succeed
|
||||
tutorial1 = temp_tutorials_dir / "working-tutorial"
|
||||
tutorial1.mkdir()
|
||||
(tutorial1 / "cell-1.md").write_text("# Working")
|
||||
|
||||
tutorial2 = temp_tutorials_dir / "broken-tutorial"
|
||||
tutorial2.mkdir()
|
||||
# Create a file that will cause an error when trying to determine cell type
|
||||
(tutorial2 / "cell-1.txt").write_text("Invalid")
|
||||
|
||||
with patch.object(
|
||||
create_tutorial_notebooks_module, "_get_tutorials_directory"
|
||||
) as mock_get_dir:
|
||||
mock_get_dir.return_value = temp_tutorials_dir
|
||||
|
||||
await create_tutorial_notebooks(user_id, mock_session)
|
||||
|
||||
# Verify working tutorial was created
|
||||
assert mock_session.add.call_count == 1
|
||||
added_notebook = mock_session.add.call_args[0][0]
|
||||
assert added_notebook.name == "Working tutorial - tutorial 🧠"
|
||||
mock_session.commit.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_tutorial_notebooks_deterministic_ids(
|
||||
self, mock_session, temp_tutorials_dir
|
||||
):
|
||||
"""Test that tutorial notebooks have deterministic IDs based on name."""
|
||||
user_id = uuid4()
|
||||
|
||||
tutorial_dir = temp_tutorials_dir / "test-tutorial"
|
||||
tutorial_dir.mkdir()
|
||||
(tutorial_dir / "cell-1.md").write_text("# Test")
|
||||
|
||||
with patch.object(
|
||||
create_tutorial_notebooks_module, "_get_tutorials_directory"
|
||||
) as mock_get_dir:
|
||||
mock_get_dir.return_value = temp_tutorials_dir
|
||||
|
||||
# Create notebooks twice
|
||||
await create_tutorial_notebooks(user_id, mock_session)
|
||||
first_notebook = mock_session.add.call_args[0][0]
|
||||
first_id = first_notebook.id
|
||||
|
||||
# Reset mocks
|
||||
mock_session.add.reset_mock()
|
||||
mock_session.commit.reset_mock()
|
||||
|
||||
await create_tutorial_notebooks(user_id, mock_session)
|
||||
second_notebook = mock_session.add.call_args[0][0]
|
||||
second_id = second_notebook.id
|
||||
|
||||
# IDs should be the same (deterministic)
|
||||
assert first_id == second_id
|
||||
assert first_id == uuid5(NAMESPACE_OID, name="Test tutorial - tutorial 🧠")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_tutorial_notebooks_with_config_deletable(
|
||||
self, mock_session, temp_tutorials_dir
|
||||
):
|
||||
"""Test that deletable flag from config.json is respected."""
|
||||
import json
|
||||
|
||||
user_id = uuid4()
|
||||
|
||||
tutorial_dir = temp_tutorials_dir / "test-tutorial"
|
||||
tutorial_dir.mkdir()
|
||||
(tutorial_dir / "cell-1.md").write_text("# Test")
|
||||
(tutorial_dir / "config.json").write_text(
|
||||
json.dumps({"name": "Test Tutorial", "deletable": True})
|
||||
)
|
||||
|
||||
with patch.object(
|
||||
create_tutorial_notebooks_module, "_get_tutorials_directory"
|
||||
) as mock_get_dir:
|
||||
mock_get_dir.return_value = temp_tutorials_dir
|
||||
|
||||
await create_tutorial_notebooks(user_id, mock_session)
|
||||
|
||||
added_notebook = mock_session.add.call_args[0][0]
|
||||
assert added_notebook.deletable is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_tutorial_notebooks_config_missing_name(
|
||||
self, mock_session, temp_tutorials_dir
|
||||
):
|
||||
"""Test that missing name in config.json falls back to formatted directory name."""
|
||||
import json
|
||||
|
||||
user_id = uuid4()
|
||||
|
||||
tutorial_dir = temp_tutorials_dir / "test-tutorial"
|
||||
tutorial_dir.mkdir()
|
||||
(tutorial_dir / "cell-1.md").write_text("# Test")
|
||||
(tutorial_dir / "config.json").write_text(json.dumps({"deletable": False}))
|
||||
|
||||
with patch.object(
|
||||
create_tutorial_notebooks_module, "_get_tutorials_directory"
|
||||
) as mock_get_dir:
|
||||
mock_get_dir.return_value = temp_tutorials_dir
|
||||
|
||||
await create_tutorial_notebooks(user_id, mock_session)
|
||||
|
||||
added_notebook = mock_session.add.call_args[0][0]
|
||||
assert added_notebook.name == "Test tutorial - tutorial 🧠"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_tutorial_notebooks_invalid_config_json(
|
||||
self, mock_session, temp_tutorials_dir
|
||||
):
|
||||
"""Test that invalid config.json is handled gracefully."""
|
||||
user_id = uuid4()
|
||||
|
||||
tutorial_dir = temp_tutorials_dir / "test-tutorial"
|
||||
tutorial_dir.mkdir()
|
||||
(tutorial_dir / "cell-1.md").write_text("# Test")
|
||||
(tutorial_dir / "config.json").write_text("{ invalid json }")
|
||||
|
||||
with patch.object(
|
||||
create_tutorial_notebooks_module, "_get_tutorials_directory"
|
||||
) as mock_get_dir:
|
||||
mock_get_dir.return_value = temp_tutorials_dir
|
||||
|
||||
# Should not raise, should fall back to formatted name
|
||||
await create_tutorial_notebooks(user_id, mock_session)
|
||||
|
||||
added_notebook = mock_session.add.call_args[0][0]
|
||||
assert added_notebook.name == "Test tutorial - tutorial 🧠"
|
||||
|
||||
|
||||
class TestNotebookFromIpynbString:
|
||||
"""Test cases for Notebook.from_ipynb_string (legacy method, still used)."""
|
||||
|
||||
def test_notebook_from_ipynb_string_success(self):
|
||||
"""Test successful creation of notebook from JSON string."""
|
||||
notebook_json = json.dumps(sample_jupyter_notebook)
|
||||
import json
|
||||
|
||||
sample_notebook = {
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": ["# Tutorial Introduction\n", "\n", "This is a tutorial notebook."],
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": None,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": ["import cognee\n", "print('Hello, Cognee!')"],
|
||||
},
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4,
|
||||
}
|
||||
|
||||
notebook_json = json.dumps(sample_notebook)
|
||||
user_id = uuid4()
|
||||
|
||||
notebook = Notebook.from_ipynb_string(
|
||||
|
|
@ -85,55 +598,11 @@ class TestTutorialNotebookCreation:
|
|||
|
||||
assert notebook.owner_id == user_id
|
||||
assert notebook.name == "String Test Notebook"
|
||||
assert len(notebook.cells) == 4 # Should skip the raw cell
|
||||
assert len(notebook.cells) == 2
|
||||
assert notebook.cells[0].type == "markdown"
|
||||
assert notebook.cells[1].type == "code"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_notebook_cell_name_generation(self, sample_jupyter_notebook):
|
||||
"""Test that cell names are generated correctly from markdown headers."""
|
||||
user_id = uuid4()
|
||||
notebook_json = json.dumps(sample_jupyter_notebook)
|
||||
|
||||
notebook = Notebook.from_ipynb_string(notebook_content=notebook_json, owner_id=user_id)
|
||||
|
||||
# Check markdown header extraction
|
||||
assert notebook.cells[0].name == "Tutorial Introduction"
|
||||
assert notebook.cells[2].name == "Step 1: Data Ingestion"
|
||||
|
||||
# Check code cell naming
|
||||
assert notebook.cells[1].name == "Code Cell"
|
||||
assert notebook.cells[3].name == "Code Cell"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_notebook_from_ipynb_string_with_default_name(self, sample_jupyter_notebook):
|
||||
"""Test notebook creation uses kernelspec display_name when no name provided."""
|
||||
user_id = uuid4()
|
||||
notebook_json = json.dumps(sample_jupyter_notebook)
|
||||
|
||||
notebook = Notebook.from_ipynb_string(notebook_content=notebook_json, owner_id=user_id)
|
||||
|
||||
assert notebook.name == "Python 3" # From kernelspec.display_name
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_notebook_from_ipynb_string_fallback_name(self):
|
||||
"""Test fallback naming when kernelspec is missing."""
|
||||
minimal_notebook = {
|
||||
"cells": [{"cell_type": "markdown", "metadata": {}, "source": ["# Test"]}],
|
||||
"metadata": {}, # No kernelspec
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4,
|
||||
}
|
||||
|
||||
user_id = uuid4()
|
||||
notebook_json = json.dumps(minimal_notebook)
|
||||
|
||||
notebook = Notebook.from_ipynb_string(notebook_content=notebook_json, owner_id=user_id)
|
||||
|
||||
assert notebook.name == "Imported Notebook" # Fallback name
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_notebook_from_ipynb_string_invalid_json(self):
|
||||
def test_notebook_from_ipynb_string_invalid_json(self):
|
||||
"""Test error handling for invalid JSON."""
|
||||
user_id = uuid4()
|
||||
invalid_json = "{ invalid json content"
|
||||
|
|
@ -142,258 +611,3 @@ class TestTutorialNotebookCreation:
|
|||
|
||||
with pytest.raises(NotJSONError):
|
||||
Notebook.from_ipynb_string(notebook_content=invalid_json, owner_id=user_id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch.object(Notebook, "from_ipynb_zip_url")
|
||||
async def test_create_tutorial_notebook_error_propagated(self, mock_from_zip_url, mock_session):
|
||||
"""Test that errors are propagated when zip fetch fails."""
|
||||
user_id = uuid4()
|
||||
mock_from_zip_url.side_effect = Exception("Network error")
|
||||
|
||||
# Should raise the exception (not catch it)
|
||||
with pytest.raises(Exception, match="Network error"):
|
||||
await _create_tutorial_notebook(user_id, mock_session)
|
||||
|
||||
# Verify error handling path was taken
|
||||
mock_from_zip_url.assert_called_once()
|
||||
mock_session.add.assert_not_called()
|
||||
mock_session.commit.assert_not_called()
|
||||
|
||||
def test_generate_cell_name_code_cell(self):
|
||||
"""Test cell name generation for code cells."""
|
||||
from nbformat.notebooknode import NotebookNode
|
||||
|
||||
mock_cell = NotebookNode(
|
||||
{"cell_type": "code", "source": 'import pandas as pd\nprint("Hello world")'}
|
||||
)
|
||||
|
||||
result = Notebook._generate_cell_name(mock_cell)
|
||||
assert result == "Code Cell"
|
||||
|
||||
|
||||
class TestTutorialNotebookZipFunctionality:
|
||||
"""Test cases for zip-based tutorial functionality."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_notebook_from_ipynb_zip_url_missing_notebook(
|
||||
self,
|
||||
):
|
||||
"""Test error handling when notebook file is missing from zip."""
|
||||
user_id = uuid4()
|
||||
|
||||
with pytest.raises(
|
||||
FileNotFoundError,
|
||||
match="Notebook file 'super_random_tutorial_name.ipynb' not found in zip",
|
||||
):
|
||||
await Notebook.from_ipynb_zip_url(
|
||||
zip_url="https://github.com/topoteretes/cognee/raw/notebook_tutorial/notebooks/starter_tutorial.zip",
|
||||
owner_id=user_id,
|
||||
notebook_filename="super_random_tutorial_name.ipynb",
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_notebook_from_ipynb_zip_url_download_failure(self):
|
||||
"""Test error handling when zip download fails."""
|
||||
user_id = uuid4()
|
||||
with pytest.raises(RuntimeError, match="Failed to download tutorial zip"):
|
||||
await Notebook.from_ipynb_zip_url(
|
||||
zip_url="https://github.com/topoteretes/cognee/raw/notebook_tutorial/notebooks/nonexistent_tutorial_name.zip",
|
||||
owner_id=user_id,
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_tutorial_notebook_zip_success(self, mock_session):
|
||||
"""Test successful tutorial notebook creation with zip."""
|
||||
await cognee.prune.prune_data()
|
||||
await cognee.prune.prune_system(metadata=True)
|
||||
|
||||
user_id = uuid4()
|
||||
|
||||
# Check that tutorial data directory is empty using storage-aware method
|
||||
tutorial_data_dir_path = await get_tutorial_data_dir()
|
||||
tutorial_data_dir = Path(tutorial_data_dir_path)
|
||||
if tutorial_data_dir.exists():
|
||||
assert not any(tutorial_data_dir.iterdir()), "Tutorial data directory should be empty"
|
||||
|
||||
await _create_tutorial_notebook(user_id, mock_session)
|
||||
|
||||
items = list(tutorial_data_dir.iterdir())
|
||||
assert len(items) == 1, "Tutorial data directory should contain exactly one item"
|
||||
assert items[0].is_dir(), "Tutorial data directory item should be a directory"
|
||||
|
||||
# Verify the structure inside the tutorial directory
|
||||
tutorial_dir = items[0]
|
||||
|
||||
# Check for tutorial.ipynb file
|
||||
notebook_file = tutorial_dir / "tutorial.ipynb"
|
||||
assert notebook_file.exists(), f"tutorial.ipynb should exist in {tutorial_dir}"
|
||||
assert notebook_file.is_file(), "tutorial.ipynb should be a file"
|
||||
|
||||
# Check for data subfolder with contents
|
||||
data_folder = tutorial_dir / "data"
|
||||
assert data_folder.exists(), f"data subfolder should exist in {tutorial_dir}"
|
||||
assert data_folder.is_dir(), "data should be a directory"
|
||||
|
||||
data_items = list(data_folder.iterdir())
|
||||
assert len(data_items) > 0, (
|
||||
f"data folder should contain files, but found {len(data_items)} items"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_tutorial_notebook_with_force_refresh(self, mock_session):
|
||||
"""Test tutorial notebook creation with force refresh."""
|
||||
await cognee.prune.prune_data()
|
||||
await cognee.prune.prune_system(metadata=True)
|
||||
|
||||
user_id = uuid4()
|
||||
|
||||
# Check that tutorial data directory is empty using storage-aware method
|
||||
tutorial_data_dir_path = await get_tutorial_data_dir()
|
||||
tutorial_data_dir = Path(tutorial_data_dir_path)
|
||||
if tutorial_data_dir.exists():
|
||||
assert not any(tutorial_data_dir.iterdir()), "Tutorial data directory should be empty"
|
||||
|
||||
# First creation (without force refresh)
|
||||
await _create_tutorial_notebook(user_id, mock_session, force_refresh=False)
|
||||
|
||||
items_first = list(tutorial_data_dir.iterdir())
|
||||
assert len(items_first) == 1, (
|
||||
"Tutorial data directory should contain exactly one item after first creation"
|
||||
)
|
||||
first_dir = items_first[0]
|
||||
assert first_dir.is_dir(), "Tutorial data directory item should be a directory"
|
||||
|
||||
# Verify the structure inside the tutorial directory (first creation)
|
||||
notebook_file = first_dir / "tutorial.ipynb"
|
||||
assert notebook_file.exists(), f"tutorial.ipynb should exist in {first_dir}"
|
||||
assert notebook_file.is_file(), "tutorial.ipynb should be a file"
|
||||
|
||||
data_folder = first_dir / "data"
|
||||
assert data_folder.exists(), f"data subfolder should exist in {first_dir}"
|
||||
assert data_folder.is_dir(), "data should be a directory"
|
||||
|
||||
data_items = list(data_folder.iterdir())
|
||||
assert len(data_items) > 0, (
|
||||
f"data folder should contain files, but found {len(data_items)} items"
|
||||
)
|
||||
|
||||
# Capture metadata from first creation
|
||||
|
||||
first_creation_metadata = {}
|
||||
|
||||
for file_path in first_dir.rglob("*"):
|
||||
if file_path.is_file():
|
||||
relative_path = file_path.relative_to(first_dir)
|
||||
stat = file_path.stat()
|
||||
|
||||
# Store multiple metadata points
|
||||
with open(file_path, "rb") as f:
|
||||
content = f.read()
|
||||
|
||||
first_creation_metadata[str(relative_path)] = {
|
||||
"mtime": stat.st_mtime,
|
||||
"size": stat.st_size,
|
||||
"hash": hashlib.md5(content).hexdigest(),
|
||||
"first_bytes": content[:100]
|
||||
if content
|
||||
else b"", # First 100 bytes as fingerprint
|
||||
}
|
||||
|
||||
# Wait a moment to ensure different timestamps
|
||||
time.sleep(0.1)
|
||||
|
||||
# Force refresh - should create new files with different metadata
|
||||
await _create_tutorial_notebook(user_id, mock_session, force_refresh=True)
|
||||
|
||||
items_second = list(tutorial_data_dir.iterdir())
|
||||
assert len(items_second) == 1, (
|
||||
"Tutorial data directory should contain exactly one item after force refresh"
|
||||
)
|
||||
second_dir = items_second[0]
|
||||
|
||||
# Verify the structure is maintained after force refresh
|
||||
notebook_file_second = second_dir / "tutorial.ipynb"
|
||||
assert notebook_file_second.exists(), (
|
||||
f"tutorial.ipynb should exist in {second_dir} after force refresh"
|
||||
)
|
||||
assert notebook_file_second.is_file(), "tutorial.ipynb should be a file after force refresh"
|
||||
|
||||
data_folder_second = second_dir / "data"
|
||||
assert data_folder_second.exists(), (
|
||||
f"data subfolder should exist in {second_dir} after force refresh"
|
||||
)
|
||||
assert data_folder_second.is_dir(), "data should be a directory after force refresh"
|
||||
|
||||
data_items_second = list(data_folder_second.iterdir())
|
||||
assert len(data_items_second) > 0, (
|
||||
f"data folder should still contain files after force refresh, but found {len(data_items_second)} items"
|
||||
)
|
||||
|
||||
# Compare metadata to ensure files are actually different
|
||||
files_with_changed_metadata = 0
|
||||
|
||||
for file_path in second_dir.rglob("*"):
|
||||
if file_path.is_file():
|
||||
relative_path = file_path.relative_to(second_dir)
|
||||
relative_path_str = str(relative_path)
|
||||
|
||||
# File should exist from first creation
|
||||
assert relative_path_str in first_creation_metadata, (
|
||||
f"File {relative_path_str} missing from first creation"
|
||||
)
|
||||
|
||||
old_metadata = first_creation_metadata[relative_path_str]
|
||||
|
||||
# Get new metadata
|
||||
stat = file_path.stat()
|
||||
with open(file_path, "rb") as f:
|
||||
new_content = f.read()
|
||||
|
||||
new_metadata = {
|
||||
"mtime": stat.st_mtime,
|
||||
"size": stat.st_size,
|
||||
"hash": hashlib.md5(new_content).hexdigest(),
|
||||
"first_bytes": new_content[:100] if new_content else b"",
|
||||
}
|
||||
|
||||
# Check if any metadata changed (indicating file was refreshed)
|
||||
metadata_changed = (
|
||||
new_metadata["mtime"] > old_metadata["mtime"] # Newer modification time
|
||||
or new_metadata["hash"] != old_metadata["hash"] # Different content hash
|
||||
or new_metadata["size"] != old_metadata["size"] # Different file size
|
||||
or new_metadata["first_bytes"]
|
||||
!= old_metadata["first_bytes"] # Different content
|
||||
)
|
||||
|
||||
if metadata_changed:
|
||||
files_with_changed_metadata += 1
|
||||
|
||||
# Assert that force refresh actually updated files
|
||||
assert files_with_changed_metadata > 0, (
|
||||
f"Force refresh should have updated at least some files, but all {len(first_creation_metadata)} "
|
||||
f"files appear to have identical metadata. This suggests force refresh didn't work."
|
||||
)
|
||||
|
||||
mock_session.commit.assert_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tutorial_zip_url_accessibility(self):
|
||||
"""Test that the actual tutorial zip URL is accessible (integration test)."""
|
||||
try:
|
||||
import requests
|
||||
|
||||
response = requests.get(
|
||||
"https://github.com/topoteretes/cognee/raw/notebook_tutorial/notebooks/starter_tutorial.zip",
|
||||
timeout=10,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
# Verify it's a valid zip file by checking headers
|
||||
assert response.headers.get("content-type") in [
|
||||
"application/zip",
|
||||
"application/octet-stream",
|
||||
"application/x-zip-compressed",
|
||||
] or response.content.startswith(b"PK") # Zip file signature
|
||||
|
||||
except Exception:
|
||||
pytest.skip("Network request failed or zip not available - skipping integration test")
|
||||
|
|
|
|||
|
|
@ -201,7 +201,8 @@ exclude = [
|
|||
"cognee/modules/users/models/Group.py",
|
||||
"cognee/modules/users/models/ACL.py",
|
||||
"cognee/modules/pipelines/models/Task.py",
|
||||
"cognee/modules/data/models/Dataset.py"
|
||||
"cognee/modules/data/models/Dataset.py",
|
||||
"cognee/modules/notebooks/tutorials/" # Ignore tutorial Python files
|
||||
]
|
||||
|
||||
[tool.ruff.lint]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue