pybricksMicropython/lib: add module

This creates a new module for dealing with stuff specific to Pybricks
MicroPython.
This commit is contained in:
David Lechner
2022-03-11 15:41:16 -06:00
parent 38b9bb6656
commit e36a30de4c
8 changed files with 142 additions and 73 deletions
+2 -1
View File
@@ -4,6 +4,7 @@
import React, { useContext } from 'react';
import { useDispatch } from 'react-redux';
import * as notificationActions from '../notifications/actions';
import { pythonFileExtension } from '../pybricksMicropython/lib';
import OpenFileButton, { OpenFileButtonProps } from '../toolbar/OpenFileButton';
import { TooltipId } from '../toolbar/i18n';
import { EditorContext } from './Editor';
@@ -18,7 +19,7 @@ const OpenButton: React.FunctionComponent<OpenButtonProps> = (props) => {
return (
<OpenFileButton
fileExtension=".py"
fileExtension={pythonFileExtension}
tooltip={TooltipId.Open}
icon={openIcon}
enabled={editor !== null}
+3 -2
View File
@@ -3,6 +3,7 @@
import FileSaver from 'file-saver';
import { call, getContext, put, takeEvery } from 'typed-redux-saga/macro';
import { pythonFileExtension, pythonFileMimeType } from '../pybricksMicropython/lib';
import { ensureError } from '../utils';
import { EditorType } from './Editor';
import { didFailToSaveAs, didSaveAs, open, saveAs } from './actions';
@@ -37,7 +38,7 @@ function* handleSaveAs(): Generator {
}
const data = editor.getValue();
const blob = new Blob([data], { type: 'text/x-python;charset=utf-8' });
const blob = new Blob([data], { type: `${pythonFileMimeType};charset=utf-8` });
if (window.showSaveFilePicker) {
// This uses https://wicg.github.io/file-system-access which is not
@@ -48,7 +49,7 @@ function* handleSaveAs(): Generator {
suggestedName: 'main.py',
types: [
{
accept: { 'text/x-python': '.py' },
accept: { [pythonFileMimeType]: pythonFileExtension },
// TODO: translate description
description: 'Python Files',
},
+9 -62
View File
@@ -14,72 +14,19 @@ import {
import { useI18n } from '@shopify/react-i18n';
import React, { useRef, useState } from 'react';
import { useDispatch } from 'react-redux';
import {
FileNameValidationResult,
pythonFileExtension,
validateFileName,
} from '../pybricksMicropython/lib';
import { useSelector } from '../reducers';
import { FileExtension, Hub, explorerCreateNewFile } from './actions';
import { Hub, explorerCreateNewFile } from './actions';
import { NewFileWizardStringId } from './i18n';
import en from './i18n.en.json';
// This should be set to the most commonly used hub.
const defaultHub = Hub.Technic;
/** File name validation results. */
enum FileNameValidationResult {
/** The file name is acceptable. */
IsOk,
/** The file name is an empty string. */
IsEmpty,
/** The file name contains spaces. */
HasSpaces,
/** The file name include the file file extension. */
HasFileExtension,
/** The first character is not a letter or underscore. */
HasInvalidFirstCharacter,
/** The file name has invalid characters. */
HasInvalidCharacters,
/** A file with the same name already exists. */
AlreadyExists,
}
/**
* Validates the file name according to a number of criteria.
*
* @param fileName The file name (without extension).
* @param extension The file extension (include ".").
* @param existingFiles List of existing files.
* @returns The result of the validation.
*/
function validateFileName(
fileName: string,
extension: string,
existingFiles: ReadonlyArray<string>,
): FileNameValidationResult {
if (existingFiles.includes(`${fileName}${extension}`)) {
return FileNameValidationResult.AlreadyExists;
}
if (fileName.length === 0) {
return FileNameValidationResult.IsEmpty;
}
if (fileName.match(/\s/)) {
return FileNameValidationResult.HasSpaces;
}
if (fileName.endsWith(extension)) {
return FileNameValidationResult.HasFileExtension;
}
if (!fileName.match(/^[a-zA-Z_]/)) {
return FileNameValidationResult.HasInvalidFirstCharacter;
}
if (!fileName.match(/^[a-zA-Z0-9_-]+$/)) {
return FileNameValidationResult.HasInvalidCharacters;
}
return FileNameValidationResult.IsOk;
}
type FileNameHelpTextProps = {
validation: FileNameValidationResult;
};
@@ -169,7 +116,7 @@ const NewFileWizard: React.VoidFunctionComponent<NewFileWizardProps> = (props) =
const handleFileNameChanged = (fileName: string) => {
setFileNameValidation(
validateFileName(fileName, FileExtension.Python, fileNames),
validateFileName(fileName, pythonFileExtension, fileNames),
);
setFileName(fileName);
};
@@ -194,7 +141,7 @@ const NewFileWizard: React.VoidFunctionComponent<NewFileWizardProps> = (props) =
value={fileName}
inputRef={fileNameInputRef}
intent={fileNameIntent}
rightElement={<Tag>{FileExtension.Python}</Tag>}
rightElement={<Tag>{pythonFileExtension}</Tag>}
onChange={(e) => handleFileNameChanged(e.target.value)}
/>
</FormGroup>
@@ -224,7 +171,7 @@ const NewFileWizard: React.VoidFunctionComponent<NewFileWizardProps> = (props) =
dispatch(
explorerCreateNewFile(
fileName,
FileExtension.Python,
pythonFileExtension,
hubType,
),
);
+3 -5
View File
@@ -2,12 +2,10 @@
// Copyright (c) 2022 The Pybricks Authors
import { createAction } from '../actions';
import { pythonFileExtension } from '../pybricksMicropython/lib';
/** Supported file extensions. */
export enum FileExtension {
/** Python (.py) */
Python = '.py',
}
type SupportedFileExtension = typeof pythonFileExtension;
/** Supported hub types. */
export enum Hub {
@@ -32,7 +30,7 @@ export enum Hub {
* @param hub The type of hub this file is for.
*/
export const explorerCreateNewFile = createAction(
(fileName: string, fileExtension: FileExtension, hub: Hub) => ({
(fileName: string, fileExtension: SupportedFileExtension, hub: Hub) => ({
type: 'explorer.action.createNewFile',
fileName,
fileExtension,
+3 -2
View File
@@ -2,14 +2,15 @@
// Copyright (c) 2022 The Pybricks Authors
import { AsyncSaga } from '../../test';
import { FileExtension, Hub, explorerCreateNewFile } from './actions';
import { pythonFileExtension } from '../pybricksMicropython/lib';
import { Hub, explorerCreateNewFile } from './actions';
import explorer from './sagas';
describe('handleExplorerCreateNewFile', () => {
it('should dispatch fileStorage action', async () => {
const saga = new AsyncSaga(explorer);
saga.put(explorerCreateNewFile('test', FileExtension.Python, Hub.Technic));
saga.put(explorerCreateNewFile('test', pythonFileExtension, Hub.Technic));
const action = await saga.take();
expect(action).toMatchInlineSnapshot(`
+2 -1
View File
@@ -9,6 +9,7 @@ import { eventChannel } from 'redux-saga';
import { call, fork, getContext, put, takeEvery } from 'typed-redux-saga/macro';
import Observable from 'zen-observable';
import { EditorType } from '../editor/Editor';
import { pythonFileExtension, pythonFileMimeType } from '../pybricksMicropython/lib';
import { ensureError, timestamp } from '../utils';
import {
fileStorageArchiveAllFiles,
@@ -117,7 +118,7 @@ function* handleExportFile(
suggestedName: action.fileName,
types: [
{
accept: { 'text/x-python': '.py' },
accept: { [pythonFileMimeType]: pythonFileExtension },
// TODO: translate description
description: 'Python Files',
},
+54
View File
@@ -0,0 +1,54 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
import { FileNameValidationResult, pythonFileExtension, validateFileName } from './lib';
describe('validateFileName', () => {
it('should allow file names with underscores', () => {
expect(validateFileName('file_name', pythonFileExtension, [])).toBe(
FileNameValidationResult.IsOk,
);
});
it('should allow file names with dashes', () => {
expect(validateFileName('file-name', pythonFileExtension, [])).toBe(
FileNameValidationResult.IsOk,
);
});
it('should not allow empty strings', () => {
expect(validateFileName('', pythonFileExtension, [])).toBe(
FileNameValidationResult.IsEmpty,
);
});
it('should not allow file names with spaces', () => {
expect(validateFileName('file name', pythonFileExtension, [])).toBe(
FileNameValidationResult.HasSpaces,
);
});
it('should not allow file names that start with numbers', () => {
expect(validateFileName('1test', pythonFileExtension, [])).toBe(
FileNameValidationResult.HasInvalidFirstCharacter,
);
});
it('should not allow file names with symbols', () => {
expect(validateFileName('test$', pythonFileExtension, [])).toBe(
FileNameValidationResult.HasInvalidCharacters,
);
});
it('it should not allow file names that include the file extension', () => {
expect(validateFileName('test.py', pythonFileExtension, [])).toBe(
FileNameValidationResult.HasFileExtension,
);
});
it('should not allow file names that match existing files', () => {
expect(validateFileName('test', pythonFileExtension, ['test.py'])).toBe(
FileNameValidationResult.AlreadyExists,
);
});
});
+66
View File
@@ -0,0 +1,66 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
/** The Python file extension ('.py') */
export const pythonFileExtension = '.py';
/** The Python file MIME type ('text/x-python') */
export const pythonFileMimeType = 'text/x-python';
/** File name validation results. */
export enum FileNameValidationResult {
/** The file name is acceptable. */
IsOk,
/** The file name is an empty string. */
IsEmpty,
/** The file name contains spaces. */
HasSpaces,
/** The file name include the file file extension. */
HasFileExtension,
/** The first character is not a letter or underscore. */
HasInvalidFirstCharacter,
/** The file name has invalid characters. */
HasInvalidCharacters,
/** A file with the same name already exists. */
AlreadyExists,
}
/**
* Validates the file name according to a number of criteria.
*
* @param fileName The file name (without extension).
* @param extension The file extension (including ".").
* @param existingFiles List of existing files.
* @returns The result of the validation.
*/
export function validateFileName(
fileName: string,
extension: string,
existingFiles: ReadonlyArray<string>,
): FileNameValidationResult {
if (existingFiles.includes(`${fileName}${extension}`)) {
return FileNameValidationResult.AlreadyExists;
}
if (fileName.length === 0) {
return FileNameValidationResult.IsEmpty;
}
if (fileName.match(/\s/)) {
return FileNameValidationResult.HasSpaces;
}
if (fileName.endsWith(extension)) {
return FileNameValidationResult.HasFileExtension;
}
if (!fileName.match(/^[a-zA-Z_]/)) {
return FileNameValidationResult.HasInvalidFirstCharacter;
}
if (!fileName.match(/^[a-zA-Z0-9_-]+$/)) {
return FileNameValidationResult.HasInvalidCharacters;
}
return FileNameValidationResult.IsOk;
}