From 4c5a619862c8073eb97b29f303e9a403f47b1c5f Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Sun, 26 Jun 2022 14:21:41 +0200 Subject: [PATCH 1/9] chore(data): add schema version to data files --- data/annotations/sklearn__annotations.json | 1 + data/api/sklearn__api.json | 1 + data/usages/sklearn__usage_counts.json | 1 + 3 files changed, 3 insertions(+) diff --git a/data/annotations/sklearn__annotations.json b/data/annotations/sklearn__annotations.json index 665d20025..e8d787689 100644 --- a/data/annotations/sklearn__annotations.json +++ b/data/annotations/sklearn__annotations.json @@ -1,4 +1,5 @@ { + "schemaVersion": 1, "constants": { "sklearn/sklearn._config/set_config/assume_finite": { "target": "sklearn/sklearn._config/set_config/assume_finite", diff --git a/data/api/sklearn__api.json b/data/api/sklearn__api.json index ca12f6ec2..751af0fa8 100644 --- a/data/api/sklearn__api.json +++ b/data/api/sklearn__api.json @@ -1,4 +1,5 @@ { + "schemaVersion": 1, "distribution": "scikit-learn", "package": "sklearn", "version": "1.1.1", diff --git a/data/usages/sklearn__usage_counts.json b/data/usages/sklearn__usage_counts.json index 6624d9470..dd2ec43ca 100644 --- a/data/usages/sklearn__usage_counts.json +++ b/data/usages/sklearn__usage_counts.json @@ -1,4 +1,5 @@ { + "schemaVersion": 1, "class_counts": { "sklearn/sklearn.preprocessing._label/LabelEncoder": 22465, "sklearn/sklearn.preprocessing._data/StandardScaler": 12598, From 3977cff4d4e929c7dab3015f4cfd6d7af312494d Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Sun, 26 Jun 2022 14:29:14 +0200 Subject: [PATCH 2/9] feat(parser): add schema version to generated files --- .../model/annotations/__init__.py | 1 + .../model/annotations/_annotations.py | 3 +++ .../package_parser/model/api/__init__.py | 1 + .../package_parser/model/api/_api.py | 2 ++ .../package_parser/model/usages/__init__.py | 2 +- .../package_parser/model/usages/_usages.py | 3 +++ package-parser/tests/model/test_annotations.py | 3 ++- package-parser/tests/model/test_usages.py | 18 ++++++++++++++++++ 8 files changed, 31 insertions(+), 2 deletions(-) diff --git a/package-parser/package_parser/model/annotations/__init__.py b/package-parser/package_parser/model/annotations/__init__.py index a24e67c70..2cdecbf96 100644 --- a/package-parser/package_parser/model/annotations/__init__.py +++ b/package-parser/package_parser/model/annotations/__init__.py @@ -1,4 +1,5 @@ from ._annotations import ( + ANNOTATION_SCHEMA_VERSION, AbstractAnnotation, AnnotationStore, BoundaryAnnotation, diff --git a/package-parser/package_parser/model/annotations/_annotations.py b/package-parser/package_parser/model/annotations/_annotations.py index cf76fcf6f..12058d330 100644 --- a/package-parser/package_parser/model/annotations/_annotations.py +++ b/package-parser/package_parser/model/annotations/_annotations.py @@ -2,6 +2,8 @@ from enum import Enum from typing import Any +ANNOTATION_SCHEMA_VERSION = 1 + @dataclass class AbstractAnnotation: @@ -86,6 +88,7 @@ def __init__(self): def to_json(self) -> dict: return { + "schemaVersion": ANNOTATION_SCHEMA_VERSION, "constants": { annotation.target: annotation.to_json() for annotation in self.constants }, diff --git a/package-parser/package_parser/model/api/__init__.py b/package-parser/package_parser/model/api/__init__.py index c119744f1..7acf71ce9 100644 --- a/package-parser/package_parser/model/api/__init__.py +++ b/package-parser/package_parser/model/api/__init__.py @@ -1,4 +1,5 @@ from ._api import ( + API_SCHEMA_VERSION, API, Class, FromImport, diff --git a/package-parser/package_parser/model/api/_api.py b/package-parser/package_parser/model/api/_api.py index 688a6b7e9..d3b088637 100644 --- a/package-parser/package_parser/model/api/_api.py +++ b/package-parser/package_parser/model/api/_api.py @@ -14,6 +14,7 @@ ) from package_parser.utils import parent_id +API_SCHEMA_VERSION = 1 class API: @staticmethod @@ -96,6 +97,7 @@ def get_default_value(self, parameter_id: str) -> Optional[str]: def to_json(self) -> Any: return { + "schemaVersion": API_SCHEMA_VERSION, "distribution": self.distribution, "package": self.package, "version": self.version, diff --git a/package-parser/package_parser/model/usages/__init__.py b/package-parser/package_parser/model/usages/__init__.py index b26ac2c33..24b0837a1 100644 --- a/package-parser/package_parser/model/usages/__init__.py +++ b/package-parser/package_parser/model/usages/__init__.py @@ -1 +1 @@ -from ._usages import UsageCountStore +from ._usages import USAGES_SCHEMA_VERSION, UsageCountStore diff --git a/package-parser/package_parser/model/usages/_usages.py b/package-parser/package_parser/model/usages/_usages.py index b421ea275..365171872 100644 --- a/package-parser/package_parser/model/usages/_usages.py +++ b/package-parser/package_parser/model/usages/_usages.py @@ -3,6 +3,8 @@ from collections import Counter from typing import Any +USAGES_SCHEMA_VERSION = 1 + ClassId = str FunctionId = str ParameterId = str @@ -184,6 +186,7 @@ def to_json(self) -> Any: """Converts this class to a dictionary, which can later be serialized as JSON.""" return { + "schemaVersion": USAGES_SCHEMA_VERSION, "class_counts": { class_id: usage_count for class_id, usage_count in self.class_usages.most_common() diff --git a/package-parser/tests/model/test_annotations.py b/package-parser/tests/model/test_annotations.py index 0723f13d9..0240df559 100644 --- a/package-parser/tests/model/test_annotations.py +++ b/package-parser/tests/model/test_annotations.py @@ -8,7 +8,7 @@ Interval, OptionalAnnotation, RemoveAnnotation, - RequiredAnnotation, + RequiredAnnotation, ANNOTATION_SCHEMA_VERSION, ) @@ -169,6 +169,7 @@ def test_annotation_store(): ) ) assert annotations.to_json() == { + "schemaVersion": ANNOTATION_SCHEMA_VERSION, "boundaries": { "test/boundary": { "target": "test/boundary", diff --git a/package-parser/tests/model/test_usages.py b/package-parser/tests/model/test_usages.py index bcefd7d02..020cc6949 100644 --- a/package-parser/tests/model/test_usages.py +++ b/package-parser/tests/model/test_usages.py @@ -3,10 +3,13 @@ import pytest from package_parser.model.usages import UsageCountStore +from package_parser.model.usages import USAGES_SCHEMA_VERSION + @pytest.fixture def usage_counts_json() -> Any: return { + "schemaVersion": USAGES_SCHEMA_VERSION, "class_counts": {"TestClass": 2}, "function_counts": {"TestClass.test_function": 2}, "parameter_counts": {"TestClass.test_function.test_parameter": 2}, @@ -31,6 +34,7 @@ def test_add_class_usage_for_new_class(usage_counts: UsageCountStore): usage_counts.add_class_usages("TestClass2") assert usage_counts.to_json() == { + "schemaVersion": USAGES_SCHEMA_VERSION, "class_counts": { "TestClass": 2, "TestClass2": 1, @@ -45,6 +49,7 @@ def test_add_class_usage_for_existing_class(usage_counts: UsageCountStore): usage_counts.add_class_usages("TestClass", 2) assert usage_counts.to_json() == { + "schemaVersion": USAGES_SCHEMA_VERSION, "class_counts": {"TestClass": 4}, "function_counts": {"TestClass.test_function": 2}, "parameter_counts": {"TestClass.test_function.test_parameter": 2}, @@ -65,6 +70,7 @@ def test_remove_class_for_existing_class(usage_counts: UsageCountStore): usage_counts.remove_class("TestClass") assert usage_counts.to_json() == { + "schemaVersion": USAGES_SCHEMA_VERSION, "class_counts": {}, "function_counts": {}, "parameter_counts": {}, @@ -76,6 +82,7 @@ def test_add_function_usages_for_new_function(usage_counts: UsageCountStore): usage_counts.add_function_usages("TestClass.test_function_2") assert usage_counts.to_json() == { + "schemaVersion": USAGES_SCHEMA_VERSION, "class_counts": {"TestClass": 2}, "function_counts": { "TestClass.test_function": 2, @@ -90,6 +97,7 @@ def test_add_function_usages_for_existing_function(usage_counts: UsageCountStore usage_counts.add_function_usages("TestClass.test_function", 2) assert usage_counts.to_json() == { + "schemaVersion": USAGES_SCHEMA_VERSION, "class_counts": {"TestClass": 2}, "function_counts": {"TestClass.test_function": 4}, "parameter_counts": {"TestClass.test_function.test_parameter": 2}, @@ -110,6 +118,7 @@ def test_remove_function_for_existing_function(usage_counts: UsageCountStore): usage_counts.remove_function("TestClass.test_function") assert usage_counts.to_json() == { + "schemaVersion": USAGES_SCHEMA_VERSION, "class_counts": {"TestClass": 2}, "function_counts": {}, "parameter_counts": {}, @@ -121,6 +130,7 @@ def test_add_parameter_usages_for_new_parameter(usage_counts: UsageCountStore): usage_counts.add_parameter_usages("TestClass.test_function.test_parameter_2") assert usage_counts.to_json() == { + "schemaVersion": USAGES_SCHEMA_VERSION, "class_counts": {"TestClass": 2}, "function_counts": {"TestClass.test_function": 2}, "parameter_counts": { @@ -135,6 +145,7 @@ def test_add_parameter_usages_for_existing_parameter(usage_counts: UsageCountSto usage_counts.add_parameter_usages("TestClass.test_function.test_parameter", 2) assert usage_counts.to_json() == { + "schemaVersion": USAGES_SCHEMA_VERSION, "class_counts": {"TestClass": 2}, "function_counts": {"TestClass.test_function": 2}, "parameter_counts": {"TestClass.test_function.test_parameter": 4}, @@ -155,6 +166,7 @@ def test_remove_parameter_for_existing_parameter(usage_counts: UsageCountStore): usage_counts.remove_parameter("TestClass.test_function.test_parameter") assert usage_counts.to_json() == { + "schemaVersion": USAGES_SCHEMA_VERSION, "class_counts": {"TestClass": 2}, "function_counts": {"TestClass.test_function": 2}, "parameter_counts": {}, @@ -166,6 +178,7 @@ def test_add_value_usages_for_new_parameter(usage_counts: UsageCountStore): usage_counts.add_value_usages("TestClass.test_function.test_parameter_2", "'test'") assert usage_counts.to_json() == { + "schemaVersion": USAGES_SCHEMA_VERSION, "class_counts": {"TestClass": 2}, "function_counts": {"TestClass.test_function": 2}, "parameter_counts": {"TestClass.test_function.test_parameter": 2}, @@ -180,6 +193,7 @@ def test_add_value_usages_for_new_value(usage_counts: UsageCountStore): usage_counts.add_value_usages("TestClass.test_function.test_parameter", "'test2'") assert usage_counts.to_json() == { + "schemaVersion": USAGES_SCHEMA_VERSION, "class_counts": {"TestClass": 2}, "function_counts": {"TestClass.test_function": 2}, "parameter_counts": {"TestClass.test_function.test_parameter": 2}, @@ -195,6 +209,7 @@ def test_add_value_usages_for_existing_parameter_and_value( usage_counts.add_value_usages("TestClass.test_function.test_parameter", "'test'", 2) assert usage_counts.to_json() == { + "schemaVersion": USAGES_SCHEMA_VERSION, "class_counts": {"TestClass": 2}, "function_counts": {"TestClass.test_function": 2}, "parameter_counts": {"TestClass.test_function.test_parameter": 2}, @@ -206,6 +221,7 @@ def test_init_value_for_new_parameter(usage_counts: UsageCountStore): usage_counts.init_value("TestClass.test_function.test_parameter_2") assert usage_counts.to_json() == { + "schemaVersion": USAGES_SCHEMA_VERSION, "class_counts": {"TestClass": 2}, "function_counts": {"TestClass.test_function": 2}, "parameter_counts": {"TestClass.test_function.test_parameter": 2}, @@ -220,6 +236,7 @@ def test_init_value_for_existing_parameter(usage_counts: UsageCountStore): usage_counts.init_value("TestClass.test_function.test_parameter") assert usage_counts.to_json() == { + "schemaVersion": USAGES_SCHEMA_VERSION, "class_counts": {"TestClass": 2}, "function_counts": {"TestClass.test_function": 2}, "parameter_counts": {"TestClass.test_function.test_parameter": 2}, @@ -329,6 +346,7 @@ def test_merge_other_into_self(usage_counts: UsageCountStore): usage_counts.merge_other_into_self(other) assert usage_counts.to_json() == { + "schemaVersion": USAGES_SCHEMA_VERSION, "class_counts": { "TestClass": 4, "TestClass2": 1, From e17d918dc9f6c524113890b172e727a0d47df173 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Sun, 26 Jun 2022 14:40:49 +0200 Subject: [PATCH 3/9] feat(gui): validate version of usages file --- .../src/features/usages/UsageImportDialog.tsx | 17 +++++++++++++++-- .../features/usages/model/UsageCountStore.ts | 9 ++++++++- .../gui/src/features/usages/usageSlice.ts | 2 +- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/api-editor/gui/src/features/usages/UsageImportDialog.tsx b/api-editor/gui/src/features/usages/UsageImportDialog.tsx index 297a79aa9..e2a0cee48 100644 --- a/api-editor/gui/src/features/usages/UsageImportDialog.tsx +++ b/api-editor/gui/src/features/usages/UsageImportDialog.tsx @@ -12,6 +12,7 @@ import { ModalHeader, ModalOverlay, Text as ChakraText, + useToast, } from '@chakra-ui/react'; import React, { useState } from 'react'; import { useAppDispatch, useAppSelector } from '../../app/hooks'; @@ -23,6 +24,7 @@ import { setUsages } from './usageSlice'; import { selectRawPythonPackage } from '../packageData/apiSlice'; export const UsageImportDialog: React.FC = function () { + const toast = useToast(); const [fileName, setFileName] = useState(''); const [newUsages, setNewUsages] = useState(); const dispatch = useAppDispatch(); @@ -31,9 +33,20 @@ export const UsageImportDialog: React.FC = function () { const submit = async () => { if (newUsages) { const parsedUsages = JSON.parse(newUsages) as UsageCountJson; - dispatch(setUsages(UsageCountStore.fromJson(parsedUsages, api))); + const usageCountStore = UsageCountStore.fromJson(parsedUsages, api) + if (usageCountStore) { + dispatch(setUsages(usageCountStore)); + close(); + } else { + toast({ + title: 'Old Usage Count File', + description: 'This file is not compatible with the current version of the API Editor.', + status: 'error', + duration: 4000, + }) + } } - close(); + }; const close = () => dispatch(toggleUsageImportDialog()); diff --git a/api-editor/gui/src/features/usages/model/UsageCountStore.ts b/api-editor/gui/src/features/usages/model/UsageCountStore.ts index afddf5578..c4bc27c4e 100644 --- a/api-editor/gui/src/features/usages/model/UsageCountStore.ts +++ b/api-editor/gui/src/features/usages/model/UsageCountStore.ts @@ -5,7 +5,10 @@ import { PythonModule } from '../../packageData/model/PythonModule'; import { PythonClass } from '../../packageData/model/PythonClass'; import { PythonFunction } from '../../packageData/model/PythonFunction'; +const EXPECTED_USAGES_SCHEMA_VERSION = 1; + export interface UsageCountJson { + schemaVersion?: number; module_counts?: { [target: string]: number; }; @@ -26,7 +29,11 @@ export interface UsageCountJson { } export class UsageCountStore { - static fromJson(json: UsageCountJson, api?: PythonPackage): UsageCountStore { + static fromJson(json: UsageCountJson, api?: PythonPackage): UsageCountStore | null { + if ((json.schemaVersion ?? 1) !== EXPECTED_USAGES_SCHEMA_VERSION) { + return null; + } + return new UsageCountStore( new Map(Object.entries(json.module_counts ?? {})), new Map(Object.entries(json.class_counts)), diff --git a/api-editor/gui/src/features/usages/usageSlice.ts b/api-editor/gui/src/features/usages/usageSlice.ts index 5183d442e..e1bdbd782 100644 --- a/api-editor/gui/src/features/usages/usageSlice.ts +++ b/api-editor/gui/src/features/usages/usageSlice.ts @@ -19,7 +19,7 @@ export const initializeUsages = createAsyncThunk('usages/initialize', async () = try { const storedUsageCountStoreJson = (await idb.get('usages')) as UsageCountJson; return { - usages: UsageCountStore.fromJson(storedUsageCountStoreJson), + usages: UsageCountStore.fromJson(storedUsageCountStoreJson) ?? new UsageCountStore(), }; } catch { return initialState; From 06a7afe807fb48087f732a4db92b79754742d6df Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Sun, 26 Jun 2022 14:50:06 +0200 Subject: [PATCH 4/9] feat(gui): validate version of API file --- .../gui/src/features/menuBar/HelpMenu.tsx | 3 +- .../packageData/PackageDataImportDialog.tsx | 35 ++++++++++++------- .../gui/src/features/packageData/apiSlice.ts | 7 ++-- .../packageData/model/PythonPackageBuilder.ts | 9 ++++- 4 files changed, 38 insertions(+), 16 deletions(-) diff --git a/api-editor/gui/src/features/menuBar/HelpMenu.tsx b/api-editor/gui/src/features/menuBar/HelpMenu.tsx index 7bf6b75e6..58df3004a 100644 --- a/api-editor/gui/src/features/menuBar/HelpMenu.tsx +++ b/api-editor/gui/src/features/menuBar/HelpMenu.tsx @@ -1,4 +1,4 @@ -import { Box, Button, Icon, Menu, MenuButton, MenuGroup, MenuItem, MenuList } from '@chakra-ui/react'; +import { Box, Button, Icon, Menu, MenuButton, MenuDivider, MenuGroup, MenuItem, MenuList } from '@chakra-ui/react'; import React from 'react'; import { FaBug, FaChevronDown, FaLightbulb } from 'react-icons/fa'; import { bugReportURL, featureRequestURL, userGuideURL } from '../externalLinks/urlBuilder'; @@ -21,6 +21,7 @@ export const HelpMenu = function () { User Guide + (); + const [newPythonPackageString, setNewPythonPackageString] = useState(); const navigate = useNavigate(); const dispatch = useAppDispatch(); const submit = async () => { - if (newPythonPackage) { - const parsedPythonPackage = JSON.parse(newPythonPackage) as PythonPackageJson; + if (newPythonPackageString) { + const pythonPackageJson = JSON.parse(newPythonPackageString) as PythonPackageJson; + const pythonPackage = parsePythonPackageJson(pythonPackageJson); + if (pythonPackage) { + dispatch(setPythonPackage(pythonPackage)); + dispatch(persistPythonPackage(pythonPackageJson)); - dispatch(setPythonPackage(parsePythonPackageJson(parsedPythonPackage))); - dispatch(persistPythonPackage(parsedPythonPackage)); - - // Reset other slices - dispatch(resetAnnotationStore()); - dispatch(resetUsages()); - dispatch(resetUIAfterAPIImport()); - navigate('/'); + // Reset other slices + dispatch(resetAnnotationStore()); + dispatch(resetUsages()); + dispatch(resetUIAfterAPIImport()); + navigate('/'); + } else { + toast({ + title: 'Old API File', + description: 'This file is not compatible with the current version of the API Editor.', + status: 'error', + duration: 4000, + }); + } } }; const close = () => dispatch(toggleAPIImportDialog()); @@ -56,7 +67,7 @@ export const PackageDataImportDialog: React.FC = function () { const reader = new FileReader(); reader.onload = () => { if (typeof reader.result === 'string') { - setNewPythonPackage(reader.result); + setNewPythonPackageString(reader.result); dispatch(resetAnnotationStore()); } }; diff --git a/api-editor/gui/src/features/packageData/apiSlice.ts b/api-editor/gui/src/features/packageData/apiSlice.ts index 647261c9d..00b0733dd 100644 --- a/api-editor/gui/src/features/packageData/apiSlice.ts +++ b/api-editor/gui/src/features/packageData/apiSlice.ts @@ -14,8 +14,10 @@ export interface APIState { // Initial state ------------------------------------------------------------------------------------------------------- +const initialPythonPackage = new PythonPackage('empty', 'empty', '0.0.1'); + const initialState: APIState = { - pythonPackage: new PythonPackage('empty', 'empty', '0.0.1'), + pythonPackage: initialPythonPackage, }; // Thunks -------------------------------------------------------------------------------------------------------------- @@ -23,8 +25,9 @@ const initialState: APIState = { export const initializePythonPackage = createAsyncThunk('api/initialize', async () => { try { const storedPythonPackageJson = (await idb.get('api')) as PythonPackageJson; + return { - pythonPackage: parsePythonPackageJson(storedPythonPackageJson), + pythonPackage: parsePythonPackageJson(storedPythonPackageJson) ?? initialPythonPackage, }; } catch { return initialState; diff --git a/api-editor/gui/src/features/packageData/model/PythonPackageBuilder.ts b/api-editor/gui/src/features/packageData/model/PythonPackageBuilder.ts index 2da973751..80d14962c 100644 --- a/api-editor/gui/src/features/packageData/model/PythonPackageBuilder.ts +++ b/api-editor/gui/src/features/packageData/model/PythonPackageBuilder.ts @@ -9,7 +9,10 @@ import { PythonParameter, PythonParameterAssignment } from './PythonParameter'; import { PythonResult } from './PythonResult'; import { PythonDeclaration } from './PythonDeclaration'; +const EXPECTED_API_SCHEMA_VERSION = 1; + export interface PythonPackageJson { + schemaVersion: number; distribution: string; package: string; version: string; @@ -18,7 +21,11 @@ export interface PythonPackageJson { functions: PythonFunctionJson[]; } -export const parsePythonPackageJson = function (packageJson: PythonPackageJson): PythonPackage { +export const parsePythonPackageJson = function (packageJson: PythonPackageJson): PythonPackage | null { + if ((packageJson.schemaVersion ?? 1) !== EXPECTED_API_SCHEMA_VERSION) { + return null; + } + const idToDeclaration = new Map(); // Functions From 3661a2990907c06394d1d85e633dac5f3a1444a9 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Sun, 26 Jun 2022 15:12:28 +0200 Subject: [PATCH 5/9] feat(gui): validate version of annotation file --- .../annotations/AnnotationImportDialog.tsx | 46 ++++++++++++++++--- .../features/annotations/annotationSlice.ts | 16 ++++++- .../packageData/PackageDataImportDialog.tsx | 10 ++++ .../packageData/model/PythonPackageBuilder.ts | 2 +- .../src/features/usages/UsageImportDialog.tsx | 10 ++++ 5 files changed, 75 insertions(+), 9 deletions(-) diff --git a/api-editor/gui/src/features/annotations/AnnotationImportDialog.tsx b/api-editor/gui/src/features/annotations/AnnotationImportDialog.tsx index 35ae08179..d2fe4f099 100644 --- a/api-editor/gui/src/features/annotations/AnnotationImportDialog.tsx +++ b/api-editor/gui/src/features/annotations/AnnotationImportDialog.tsx @@ -12,30 +12,64 @@ import { ModalHeader, ModalOverlay, Text as ChakraText, + useToast, } from '@chakra-ui/react'; import React, { useState } from 'react'; import { useAppDispatch } from '../../app/hooks'; import { StyledDropzone } from '../../common/StyledDropzone'; import { isValidJsonFile } from '../../common/util/validation'; -import { AnnotationStore, initialAnnotationStore, mergeAnnotationStore, setAnnotationStore } from './annotationSlice'; +import { + AnnotationStore, + EXPECTED_ANNOTATION_STORE_SCHEMA_VERSION, + initialAnnotationStore, + mergeAnnotationStore, + setAnnotationStore, + VersionedAnnotationStore, +} from './annotationSlice'; import { hideAnnotationImportDialog, toggleAnnotationImportDialog } from '../ui/uiSlice'; export const AnnotationImportDialog: React.FC = function () { + const toast = useToast(); const [fileName, setFileName] = useState(''); - const [newAnnotationStore, setNewAnnotationStore] = useState(initialAnnotationStore); + const [newAnnotationStore, setNewAnnotationStore] = useState(initialAnnotationStore); const dispatch = useAppDispatch(); + const validate = () => { + if (!fileName) { + toast({ + title: 'No File Selected', + description: 'Select a file to import or cancel this dialog.', + status: 'error', + duration: 4000, + }); + return false; + } + + if ((newAnnotationStore.schemaVersion ?? 1) !== EXPECTED_ANNOTATION_STORE_SCHEMA_VERSION) { + toast({ + title: 'Old Annotation File', + description: 'This file is not compatible with the current version of the API Editor.', + status: 'error', + duration: 4000, + }); + return false; + } + + return true; + }; const merge = () => { - if (fileName) { + if (validate()) { + delete newAnnotationStore.schemaVersion; dispatch(mergeAnnotationStore(newAnnotationStore)); + dispatch(hideAnnotationImportDialog()); } - dispatch(hideAnnotationImportDialog()); }; const replace = () => { - if (fileName) { + if (validate()) { + delete newAnnotationStore.schemaVersion; dispatch(setAnnotationStore(newAnnotationStore)); + dispatch(hideAnnotationImportDialog()); } - dispatch(hideAnnotationImportDialog()); }; const close = () => dispatch(toggleAnnotationImportDialog()); diff --git a/api-editor/gui/src/features/annotations/annotationSlice.ts b/api-editor/gui/src/features/annotations/annotationSlice.ts index 73dbe8149..e753cdf9b 100644 --- a/api-editor/gui/src/features/annotations/annotationSlice.ts +++ b/api-editor/gui/src/features/annotations/annotationSlice.ts @@ -11,16 +11,20 @@ export const maximumNumberOfClassAnnotations = 5; /** * How many annotations can be applied to a function at once. */ -export const maximumNumberOfFunctionAnnotations = 7; +export const maximumNumberOfFunctionAnnotations = 8; /** * How many annotations can be applied to a parameter at once. */ -export const maximumNumberOfParameterAnnotations = 8; +export const maximumNumberOfParameterAnnotations = 9; const maximumUndoHistoryLength = 10; +export const EXPECTED_ANNOTATION_STORE_SCHEMA_VERSION = 1; +export const EXPECTED_ANNOTATION_SLICE_SCHEMA_VERSION = 1; + export interface AnnotationSlice { + schemaVersion?: number; annotations: AnnotationStore; queue: AnnotationStore[]; queueIndex: number; @@ -75,6 +79,10 @@ export interface AnnotationStore { }; } +export interface VersionedAnnotationStore extends AnnotationStore { + schemaVersion?: number; +} + export interface Annotation { /** * ID of the annotated Python declaration. @@ -301,6 +309,10 @@ export const initialAnnotationSlice: AnnotationSlice = { export const initializeAnnotations = createAsyncThunk('annotations/initialize', async () => { try { const storedAnnotations = (await idb.get('annotations')) as AnnotationSlice; + if ((storedAnnotations.schemaVersion ?? 1) !== EXPECTED_ANNOTATION_SLICE_SCHEMA_VERSION) { + return initialAnnotationSlice; + } + return { ...initialAnnotationSlice, ...storedAnnotations, diff --git a/api-editor/gui/src/features/packageData/PackageDataImportDialog.tsx b/api-editor/gui/src/features/packageData/PackageDataImportDialog.tsx index 64034aa24..d863f8c9e 100644 --- a/api-editor/gui/src/features/packageData/PackageDataImportDialog.tsx +++ b/api-editor/gui/src/features/packageData/PackageDataImportDialog.tsx @@ -33,6 +33,16 @@ export const PackageDataImportDialog: React.FC = function () { const dispatch = useAppDispatch(); const submit = async () => { + if (!fileName) { + toast({ + title: 'No File Selected', + description: 'Select a file to import or cancel this dialog.', + status: 'error', + duration: 4000, + }); + return; + } + if (newPythonPackageString) { const pythonPackageJson = JSON.parse(newPythonPackageString) as PythonPackageJson; const pythonPackage = parsePythonPackageJson(pythonPackageJson); diff --git a/api-editor/gui/src/features/packageData/model/PythonPackageBuilder.ts b/api-editor/gui/src/features/packageData/model/PythonPackageBuilder.ts index 80d14962c..3765702c1 100644 --- a/api-editor/gui/src/features/packageData/model/PythonPackageBuilder.ts +++ b/api-editor/gui/src/features/packageData/model/PythonPackageBuilder.ts @@ -12,7 +12,7 @@ import { PythonDeclaration } from './PythonDeclaration'; const EXPECTED_API_SCHEMA_VERSION = 1; export interface PythonPackageJson { - schemaVersion: number; + schemaVersion?: number; distribution: string; package: string; version: string; diff --git a/api-editor/gui/src/features/usages/UsageImportDialog.tsx b/api-editor/gui/src/features/usages/UsageImportDialog.tsx index e2a0cee48..3ab6c7920 100644 --- a/api-editor/gui/src/features/usages/UsageImportDialog.tsx +++ b/api-editor/gui/src/features/usages/UsageImportDialog.tsx @@ -31,6 +31,16 @@ export const UsageImportDialog: React.FC = function () { const api = useAppSelector(selectRawPythonPackage); const submit = async () => { + if (!fileName) { + toast({ + title: 'No File Selected', + description: 'Select a file to import or cancel this dialog.', + status: 'error', + duration: 4000, + }); + return; + } + if (newUsages) { const parsedUsages = JSON.parse(newUsages) as UsageCountJson; const usageCountStore = UsageCountStore.fromJson(parsedUsages, api) From 73838e0244c0de7b3652afd1fb60ad95a3307382 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Sun, 26 Jun 2022 15:17:38 +0200 Subject: [PATCH 6/9] feat(gui): validate version of UI data --- api-editor/gui/src/features/annotations/annotationSlice.ts | 6 ++++-- api-editor/gui/src/features/ui/uiSlice.ts | 7 +++++++ .../gui/src/features/usages/model/UsageCountStore.ts | 1 + 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/api-editor/gui/src/features/annotations/annotationSlice.ts b/api-editor/gui/src/features/annotations/annotationSlice.ts index e753cdf9b..166763f3f 100644 --- a/api-editor/gui/src/features/annotations/annotationSlice.ts +++ b/api-editor/gui/src/features/annotations/annotationSlice.ts @@ -3,6 +3,9 @@ import * as idb from 'idb-keyval'; import { RootState } from '../../app/store'; import { isValidUsername } from '../../common/util/validation'; +export const EXPECTED_ANNOTATION_STORE_SCHEMA_VERSION = 1; +export const EXPECTED_ANNOTATION_SLICE_SCHEMA_VERSION = 1; + /** * How many annotations can be applied to a class at once. */ @@ -20,8 +23,7 @@ export const maximumNumberOfParameterAnnotations = 9; const maximumUndoHistoryLength = 10; -export const EXPECTED_ANNOTATION_STORE_SCHEMA_VERSION = 1; -export const EXPECTED_ANNOTATION_SLICE_SCHEMA_VERSION = 1; + export interface AnnotationSlice { schemaVersion?: number; diff --git a/api-editor/gui/src/features/ui/uiSlice.ts b/api-editor/gui/src/features/ui/uiSlice.ts index 6a39418a5..5b7ccd480 100644 --- a/api-editor/gui/src/features/ui/uiSlice.ts +++ b/api-editor/gui/src/features/ui/uiSlice.ts @@ -8,12 +8,15 @@ import { PythonDeclaration } from '../packageData/model/PythonDeclaration'; import { UsageCountStore } from '../usages/model/UsageCountStore'; import { selectUsages } from '../usages/usageSlice'; +const EXPECTED_UI_SCHEMA_VERSION = 1; + export interface Filter { filter: string; name: string; } export interface UIState { + schemaVersion?: number; showAnnotationImportDialog: boolean; showAPIImportDialog: boolean; showUsageImportDialog: boolean; @@ -162,6 +165,10 @@ export const initialState: UIState = { export const initializeUI = createAsyncThunk('ui/initialize', async () => { try { const storedState = (await idb.get('ui')) as UIState; + if ((storedState.schemaVersion ?? 1) !== EXPECTED_UI_SCHEMA_VERSION) { + return initialState; + } + return { ...initialState, ...storedState, diff --git a/api-editor/gui/src/features/usages/model/UsageCountStore.ts b/api-editor/gui/src/features/usages/model/UsageCountStore.ts index c4bc27c4e..291cbf96a 100644 --- a/api-editor/gui/src/features/usages/model/UsageCountStore.ts +++ b/api-editor/gui/src/features/usages/model/UsageCountStore.ts @@ -93,6 +93,7 @@ export class UsageCountStore { toJson(): UsageCountJson { return { + schemaVersion: EXPECTED_USAGES_SCHEMA_VERSION, module_counts: Object.fromEntries(this.moduleUsages), class_counts: Object.fromEntries(this.classUsages), function_counts: Object.fromEntries(this.functionUsages), From 8e3c9d05b6da46a43b2f26368d13c7ddfd8f050d Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Sun, 26 Jun 2022 15:27:56 +0200 Subject: [PATCH 7/9] feat(gui): clear API data in IndexedDB --- .../features/annotations/annotationSlice.ts | 2 -- .../gui/src/features/packageData/apiSlice.ts | 19 ++++++++++++++++++- .../packageData/model/PythonPackageBuilder.ts | 3 +-- .../features/usages/model/UsageCountStore.ts | 3 +-- .../gui/src/features/usages/usageSlice.ts | 2 ++ 5 files changed, 22 insertions(+), 7 deletions(-) diff --git a/api-editor/gui/src/features/annotations/annotationSlice.ts b/api-editor/gui/src/features/annotations/annotationSlice.ts index 166763f3f..2bf7ac442 100644 --- a/api-editor/gui/src/features/annotations/annotationSlice.ts +++ b/api-editor/gui/src/features/annotations/annotationSlice.ts @@ -23,8 +23,6 @@ export const maximumNumberOfParameterAnnotations = 9; const maximumUndoHistoryLength = 10; - - export interface AnnotationSlice { schemaVersion?: number; annotations: AnnotationStore; diff --git a/api-editor/gui/src/features/packageData/apiSlice.ts b/api-editor/gui/src/features/packageData/apiSlice.ts index 00b0733dd..8f65f0414 100644 --- a/api-editor/gui/src/features/packageData/apiSlice.ts +++ b/api-editor/gui/src/features/packageData/apiSlice.ts @@ -8,12 +8,24 @@ import { selectUsages } from '../usages/usageSlice'; import { selectAnnotationStore } from '../annotations/annotationSlice'; import { PythonDeclaration } from './model/PythonDeclaration'; +export const EXPECTED_API_SCHEMA_VERSION = 1; + export interface APIState { pythonPackage: PythonPackage; } // Initial state ------------------------------------------------------------------------------------------------------- +const initialPythonPackageJson: PythonPackageJson = { + schemaVersion: EXPECTED_API_SCHEMA_VERSION, + distribution: 'empty', + package: 'empty', + version: '0.0.1', + modules: [], + classes: [], + functions: [], +}; + const initialPythonPackage = new PythonPackage('empty', 'empty', '0.0.1'); const initialState: APIState = { @@ -25,9 +37,14 @@ const initialState: APIState = { export const initializePythonPackage = createAsyncThunk('api/initialize', async () => { try { const storedPythonPackageJson = (await idb.get('api')) as PythonPackageJson; + const pythonPackage = parsePythonPackageJson(storedPythonPackageJson); + if (!pythonPackage) { + await idb.set('api', initialPythonPackageJson); + return initialState; + } return { - pythonPackage: parsePythonPackageJson(storedPythonPackageJson) ?? initialPythonPackage, + pythonPackage, }; } catch { return initialState; diff --git a/api-editor/gui/src/features/packageData/model/PythonPackageBuilder.ts b/api-editor/gui/src/features/packageData/model/PythonPackageBuilder.ts index 3765702c1..2d3b3c54d 100644 --- a/api-editor/gui/src/features/packageData/model/PythonPackageBuilder.ts +++ b/api-editor/gui/src/features/packageData/model/PythonPackageBuilder.ts @@ -8,8 +8,7 @@ import { PythonPackage } from './PythonPackage'; import { PythonParameter, PythonParameterAssignment } from './PythonParameter'; import { PythonResult } from './PythonResult'; import { PythonDeclaration } from './PythonDeclaration'; - -const EXPECTED_API_SCHEMA_VERSION = 1; +import { EXPECTED_API_SCHEMA_VERSION } from '../apiSlice'; export interface PythonPackageJson { schemaVersion?: number; diff --git a/api-editor/gui/src/features/usages/model/UsageCountStore.ts b/api-editor/gui/src/features/usages/model/UsageCountStore.ts index 291cbf96a..fefaca8a6 100644 --- a/api-editor/gui/src/features/usages/model/UsageCountStore.ts +++ b/api-editor/gui/src/features/usages/model/UsageCountStore.ts @@ -4,8 +4,7 @@ import { PythonDeclaration } from '../../packageData/model/PythonDeclaration'; import { PythonModule } from '../../packageData/model/PythonModule'; import { PythonClass } from '../../packageData/model/PythonClass'; import { PythonFunction } from '../../packageData/model/PythonFunction'; - -const EXPECTED_USAGES_SCHEMA_VERSION = 1; +import { EXPECTED_USAGES_SCHEMA_VERSION } from '../usageSlice'; export interface UsageCountJson { schemaVersion?: number; diff --git a/api-editor/gui/src/features/usages/usageSlice.ts b/api-editor/gui/src/features/usages/usageSlice.ts index e1bdbd782..dd34a35ab 100644 --- a/api-editor/gui/src/features/usages/usageSlice.ts +++ b/api-editor/gui/src/features/usages/usageSlice.ts @@ -3,6 +3,8 @@ import { RootState } from '../../app/store'; import { UsageCountJson, UsageCountStore } from './model/UsageCountStore'; import * as idb from 'idb-keyval'; +export const EXPECTED_USAGES_SCHEMA_VERSION = 1; + export interface UsageState { usages: UsageCountStore; } From ff2accb9893b83065f4c54e62e972f3723e26ad3 Mon Sep 17 00:00:00 2001 From: lars-reimann Date: Sun, 26 Jun 2022 13:33:19 +0000 Subject: [PATCH 8/9] style: apply automatic fixes of linters --- api-editor/gui/src/features/usages/UsageImportDialog.tsx | 5 ++--- package-parser/package_parser/model/api/__init__.py | 2 +- package-parser/package_parser/model/api/_api.py | 1 + package-parser/tests/model/test_annotations.py | 3 ++- package-parser/tests/model/test_usages.py | 4 +--- 5 files changed, 7 insertions(+), 8 deletions(-) diff --git a/api-editor/gui/src/features/usages/UsageImportDialog.tsx b/api-editor/gui/src/features/usages/UsageImportDialog.tsx index 3ab6c7920..2720d7c0c 100644 --- a/api-editor/gui/src/features/usages/UsageImportDialog.tsx +++ b/api-editor/gui/src/features/usages/UsageImportDialog.tsx @@ -43,7 +43,7 @@ export const UsageImportDialog: React.FC = function () { if (newUsages) { const parsedUsages = JSON.parse(newUsages) as UsageCountJson; - const usageCountStore = UsageCountStore.fromJson(parsedUsages, api) + const usageCountStore = UsageCountStore.fromJson(parsedUsages, api); if (usageCountStore) { dispatch(setUsages(usageCountStore)); close(); @@ -53,10 +53,9 @@ export const UsageImportDialog: React.FC = function () { description: 'This file is not compatible with the current version of the API Editor.', status: 'error', duration: 4000, - }) + }); } } - }; const close = () => dispatch(toggleUsageImportDialog()); diff --git a/package-parser/package_parser/model/api/__init__.py b/package-parser/package_parser/model/api/__init__.py index 7acf71ce9..32007cdd3 100644 --- a/package-parser/package_parser/model/api/__init__.py +++ b/package-parser/package_parser/model/api/__init__.py @@ -1,6 +1,6 @@ from ._api import ( - API_SCHEMA_VERSION, API, + API_SCHEMA_VERSION, Class, FromImport, Function, diff --git a/package-parser/package_parser/model/api/_api.py b/package-parser/package_parser/model/api/_api.py index d3b088637..97f5fab35 100644 --- a/package-parser/package_parser/model/api/_api.py +++ b/package-parser/package_parser/model/api/_api.py @@ -16,6 +16,7 @@ API_SCHEMA_VERSION = 1 + class API: @staticmethod def from_json(json: Any) -> API: diff --git a/package-parser/tests/model/test_annotations.py b/package-parser/tests/model/test_annotations.py index 0240df559..b7011bd78 100644 --- a/package-parser/tests/model/test_annotations.py +++ b/package-parser/tests/model/test_annotations.py @@ -1,4 +1,5 @@ from package_parser.model.annotations import ( + ANNOTATION_SCHEMA_VERSION, AbstractAnnotation, AnnotationStore, BoundaryAnnotation, @@ -8,7 +9,7 @@ Interval, OptionalAnnotation, RemoveAnnotation, - RequiredAnnotation, ANNOTATION_SCHEMA_VERSION, + RequiredAnnotation, ) diff --git a/package-parser/tests/model/test_usages.py b/package-parser/tests/model/test_usages.py index 020cc6949..be4079139 100644 --- a/package-parser/tests/model/test_usages.py +++ b/package-parser/tests/model/test_usages.py @@ -1,9 +1,7 @@ from typing import Any import pytest -from package_parser.model.usages import UsageCountStore - -from package_parser.model.usages import USAGES_SCHEMA_VERSION +from package_parser.model.usages import USAGES_SCHEMA_VERSION, UsageCountStore @pytest.fixture From e75100b81c0919065c0b7e96fc74c91ee3abfaf0 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Sun, 26 Jun 2022 15:37:53 +0200 Subject: [PATCH 9/9] fix(gui): circular import --- api-editor/gui/src/features/usages/model/UsageCountStore.ts | 3 ++- api-editor/gui/src/features/usages/usageSlice.ts | 2 -- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/api-editor/gui/src/features/usages/model/UsageCountStore.ts b/api-editor/gui/src/features/usages/model/UsageCountStore.ts index fefaca8a6..b2ef8390a 100644 --- a/api-editor/gui/src/features/usages/model/UsageCountStore.ts +++ b/api-editor/gui/src/features/usages/model/UsageCountStore.ts @@ -4,7 +4,8 @@ import { PythonDeclaration } from '../../packageData/model/PythonDeclaration'; import { PythonModule } from '../../packageData/model/PythonModule'; import { PythonClass } from '../../packageData/model/PythonClass'; import { PythonFunction } from '../../packageData/model/PythonFunction'; -import { EXPECTED_USAGES_SCHEMA_VERSION } from '../usageSlice'; + +export const EXPECTED_USAGES_SCHEMA_VERSION = 1; export interface UsageCountJson { schemaVersion?: number; diff --git a/api-editor/gui/src/features/usages/usageSlice.ts b/api-editor/gui/src/features/usages/usageSlice.ts index dd34a35ab..e1bdbd782 100644 --- a/api-editor/gui/src/features/usages/usageSlice.ts +++ b/api-editor/gui/src/features/usages/usageSlice.ts @@ -3,8 +3,6 @@ import { RootState } from '../../app/store'; import { UsageCountJson, UsageCountStore } from './model/UsageCountStore'; import * as idb from 'idb-keyval'; -export const EXPECTED_USAGES_SCHEMA_VERSION = 1; - export interface UsageState { usages: UsageCountStore; }