Remove deprecated elements and adds deprecation banner to pys-on (#1084)

* Show deprecation banner

* Add test for deprecation warning

* Remove deprecated elements

* Add entry in changelog

* Update test_style

* Remove random color rule

* Add PR link to changelog
This commit is contained in:
Fábio Rosado
2023-01-03 13:14:20 +00:00
committed by GitHub
parent 5c67384fbf
commit dbdcd0b3d0
18 changed files with 30 additions and 663 deletions

View File

@@ -1,24 +1,14 @@
import type { Runtime } from '../runtime';
import { make_PyRepl } from './pyrepl';
import { PyBox } from './pybox';
import { make_PyButton } from './pybutton';
import { PyTitle } from './pytitle';
import { make_PyInputBox } from './pyinputbox';
import { make_PyWidget } from './pywidget';
function createCustomElements(runtime: Runtime) {
const PyInputBox = make_PyInputBox(runtime);
const PyButton = make_PyButton(runtime);
const PyWidget = make_PyWidget(runtime);
const PyRepl = make_PyRepl(runtime);
/* eslint-disable @typescript-eslint/no-unused-vars */
const xPyRepl = customElements.define('py-repl', PyRepl);
const xPyBox = customElements.define('py-box', PyBox);
const xPyTitle = customElements.define('py-title', PyTitle);
const xPyWidget = customElements.define('py-register-widget', PyWidget);
const xPyInputBox = customElements.define('py-inputbox', PyInputBox);
const xPyButton = customElements.define('py-button', PyButton);
/* eslint-enable @typescript-eslint/no-unused-vars */
}

View File

@@ -1,76 +0,0 @@
import { getAttribute, addClasses, createDeprecationWarning } from '../utils';
import { getLogger } from '../logger';
const logger = getLogger('py-box');
export class PyBox extends HTMLElement {
shadow: ShadowRoot;
wrapper: HTMLElement;
theme: string;
widths: string[];
constructor() {
super();
// attach shadow so we can preserve the element original innerHtml content
this.shadow = this.attachShadow({ mode: 'open' });
this.wrapper = document.createElement('slot');
this.shadow.appendChild(this.wrapper);
}
connectedCallback() {
const deprecationMessage =
'The element <py-box> is deprecated, you should create a ' +
'div with "py-box" class name instead. For example: <div class="py-box">';
createDeprecationWarning(deprecationMessage, 'py-box');
const mainDiv = document.createElement('div');
addClasses(mainDiv, ['py-box']);
// Hack: for some reason when moving children, the editor box duplicates children
// meaning that we end up with 2 editors, if there's a <py-repl> inside the <py-box>
// so, if we have more than 2 children with the cm-editor class, we remove one of them
while (this.childNodes.length > 0) {
if (this.firstChild.nodeName == 'PY-REPL') {
// in this case we need to remove the child and create a new one from scratch
const replDiv = document.createElement('div');
// we need to put the new repl inside a div so that if the repl has auto-generate true
// it can replicate itself inside that constrained div
replDiv.appendChild(this.firstChild.cloneNode());
mainDiv.appendChild(replDiv);
this.firstChild.remove();
} else {
if (this.firstChild.nodeName != '#text') {
mainDiv.appendChild(this.firstChild);
} else {
this.firstChild.remove();
}
}
}
// now we need to set widths
this.widths = [];
const widthsAttr = getAttribute(this, 'widths');
if (widthsAttr) {
for (const w of widthsAttr.split(';')) {
if (w.includes('/')) {
this.widths.push(w.split('/')[0]);
} else {
this.widths.push(w);
}
}
} else {
this.widths = Array<string>(mainDiv.children.length).fill('1 1 0');
}
this.widths.forEach((width, index) => {
const node: ChildNode = mainDiv.childNodes[index];
(<HTMLElement>node).style.flex = width;
addClasses(<HTMLElement>node, ['py-box-child']);
});
this.appendChild(mainDiv);
logger.info('py-box connected');
}
}

View File

@@ -1,85 +0,0 @@
import { getAttribute, addClasses, htmlDecode, ensureUniqueId, createDeprecationWarning } from '../utils';
import { getLogger } from '../logger';
import type { Runtime } from '../runtime';
const logger = getLogger('py-button');
export function make_PyButton(runtime: Runtime) {
class PyButton extends HTMLElement {
widths: string[] = [];
label: string | undefined = undefined;
class: string[];
defaultClass: string[];
mount_name: string | undefined = undefined;
code: string;
constructor() {
super();
this.defaultClass = ['py-button'];
const label = getAttribute(this, 'label');
if (label) {
this.label = label;
}
// Styling does the same thing as class in normal HTML. Using the name "class" makes the style to malfunction
const styling = getAttribute(this, 'styling');
if (styling) {
const klass = styling.trim();
if (klass === '') {
this.class = this.defaultClass;
} else {
// trim each element to remove unnecessary spaces which makes the button style to malfunction
this.class = klass
.split(' ')
.map(x => x.trim())
.filter(x => x !== '');
}
} else {
this.class = this.defaultClass;
}
}
connectedCallback() {
const deprecationMessage =
'The element <py-button> is deprecated, create a function with your ' +
'inline code and use <button py-click="function()" class="py-button"> instead.';
createDeprecationWarning(deprecationMessage, 'py-button');
ensureUniqueId(this);
this.code = htmlDecode(this.innerHTML) || '';
this.mount_name = this.id.split('-').join('_');
this.innerHTML = '';
const mainDiv = document.createElement('button');
mainDiv.innerHTML = this.label;
addClasses(mainDiv, this.class);
mainDiv.id = this.id;
this.id = `${this.id}-container`;
this.appendChild(mainDiv);
this.code = this.code.split('self').join(this.mount_name);
let registrationCode = `from pyodide.ffi import create_proxy`;
registrationCode += `\n${this.mount_name} = Element("${mainDiv.id}")`;
if (this.code.includes('def on_focus')) {
this.code = this.code.replace('def on_focus', `def on_focus_${this.mount_name}`);
registrationCode += `\n${this.mount_name}.element.addEventListener('focus', create_proxy(on_focus_${this.mount_name}))`;
}
if (this.code.includes('def on_click')) {
this.code = this.code.replace('def on_click', `def on_click_${this.mount_name}`);
registrationCode += `\n${this.mount_name}.element.addEventListener('click', create_proxy(on_click_${this.mount_name}))`;
}
// now that we appended and the element is attached, lets connect with the event handlers
// defined for this widget
runtime.runButDontRaise(this.code);
runtime.runButDontRaise(registrationCode);
logger.debug('py-button connected');
}
}
return PyButton;
}

View File

@@ -1,58 +0,0 @@
import { getAttribute, addClasses, htmlDecode, ensureUniqueId, createDeprecationWarning } from '../utils';
import { getLogger } from '../logger';
import type { Runtime } from '../runtime';
const logger = getLogger('py-inputbox');
export function make_PyInputBox(runtime: Runtime) {
class PyInputBox extends HTMLElement {
widths: string[] = [];
label: string | undefined = undefined;
mount_name: string | undefined = undefined;
code: string;
constructor() {
super();
const label = getAttribute(this, 'label');
if (label) {
this.label = label;
}
}
connectedCallback() {
const deprecationMessage =
'The element <py-input> is deprecated, ' + 'use <input class="py-input"> instead.';
createDeprecationWarning(deprecationMessage, 'py-input');
ensureUniqueId(this);
this.code = htmlDecode(this.innerHTML);
this.mount_name = this.id.split('-').join('_');
this.innerHTML = '';
const mainDiv = document.createElement('input');
mainDiv.type = 'text';
addClasses(mainDiv, ['py-input']);
mainDiv.id = this.id;
this.id = `${this.id}-container`;
this.appendChild(mainDiv);
// now that we appended and the element is attached, lets connect with the event handlers
// defined for this widget
this.appendChild(mainDiv);
this.code = this.code.split('self').join(this.mount_name);
let registrationCode = `from pyodide.ffi import create_proxy`;
registrationCode += `\n${this.mount_name} = Element("${mainDiv.id}")`;
if (this.code.includes('def on_keypress')) {
this.code = this.code.replace('def on_keypress', `def on_keypress_${this.mount_name}`);
registrationCode += `\n${this.mount_name}.element.addEventListener('keypress', create_proxy(on_keypress_${this.mount_name}))`;
}
runtime.runButDontRaise(this.code);
runtime.runButDontRaise(registrationCode);
logger.debug('py-inputbox connected');
}
}
return PyInputBox;
}

View File

@@ -1,4 +1,4 @@
import { htmlDecode, ensureUniqueId, showWarning } from '../utils';
import { htmlDecode, ensureUniqueId, showWarning, createDeprecationWarning } from '../utils';
import type { Runtime } from '../runtime';
import { getLogger } from '../logger';
import { pyExec } from '../pyexec';
@@ -165,9 +165,10 @@ function createElementsWithEventListeners(runtime: Runtime, pyAttribute: string)
const event = pyAttributeToEvent.get(pyAttribute);
if (pyAttribute === 'pys-onClick' || pyAttribute === 'pys-onKeyDown') {
console.warn(
'Use of pys-onClick and pys-onKeyDown attributes is deprecated in favor of py-onClick() and py-onKeyDown(). pys-on* attributes will be deprecated in a future version of PyScript.',
);
const msg =
`The attribute 'pys-onClick' and 'pys-onKeyDown' are deprecated. Please 'py-click="myFunction()"' ` +
` or 'py-keydown="myFunction()"' instead.`;
createDeprecationWarning(msg, msg);
const source = `
from pyodide.ffi import create_proxy
Element("${el.id}").element.addEventListener("${event}", create_proxy(${handlerCode}))

View File

@@ -1,29 +0,0 @@
import { addClasses, htmlDecode, createDeprecationWarning } from '../utils';
export class PyTitle extends HTMLElement {
widths: string[];
label: string;
mount_name: string;
constructor() {
super();
}
connectedCallback() {
const deprecationMessage = 'The element <py-title> is deprecated, please use an <h1> tag instead.';
createDeprecationWarning(deprecationMessage, 'py-title');
this.label = htmlDecode(this.innerHTML);
this.mount_name = this.id.split('-').join('_');
this.innerHTML = '';
const mainDiv = document.createElement('div');
const divContent = document.createElement('h1');
addClasses(mainDiv, ['py-title']);
divContent.innerHTML = this.label;
mainDiv.id = this.id;
this.id = `${this.id}-container`;
mainDiv.appendChild(divContent);
this.appendChild(mainDiv);
}
}

View File

@@ -89,7 +89,6 @@ html {
opacity: 1;
}
color: #0f172a;
.py-pop-up {
text-align: center;
width: 600px;

View File

@@ -292,3 +292,22 @@ class TestBasic(PyScriptTest):
pyscript_tag.evaluate("node => node.getPySrc()")
== 'print("hello world!")\n'
)
def test_pys_onClick_shows_deprecation_warning(self):
self.pyscript_run(
"""
<button id="1" pys-onClick="myfunc()">Click me</button>
<py-script>
def myfunc():
print("hello world")
</py-script>
"""
)
banner = self.page.locator(".alert-banner")
expected_message = (
"The attribute 'pys-onClick' and 'pys-onKeyDown' are "
"deprecated. Please 'py-click=\"myFunction()\"' or "
"'py-keydown=\"myFunction()\"' instead."
)
assert banner.inner_text() == expected_message

View File

@@ -1,34 +0,0 @@
from .support import PyScriptTest
class TestPyButton(PyScriptTest):
def test_box(self):
self.pyscript_run(
"""
<py-box>
<py-box>
</py-box>
</py-box>
"""
)
pybox_element = self.page.query_selector_all("py-box")
assert len(pybox_element) == 2
assert pybox_element[1].get_attribute("class") == "py-box-child"
def test_deprecated_element(self):
self.pyscript_run(
"""
<py-box>
</py-box>
"""
)
banner = self.page.locator(".py-warning")
banner_content = banner.inner_text()
expected = (
"The element <py-box> is deprecated, you should create a div "
'with "py-box" class name instead. For example: <div class="py-box">'
)
assert banner_content == expected

View File

@@ -1,60 +0,0 @@
from .support import PyScriptTest
class TestPyButton(PyScriptTest):
def test_on_click(self):
self.pyscript_run(
"""
<py-button label="my button">
import js
def on_click(evt):
js.console.log('clicked!')
</py-button>
"""
)
assert self.console.log.lines[0] == self.PY_COMPLETE
self.page.locator("text=my button").click()
self.page.locator("text=my button").click()
assert self.console.log.lines[-2:] == ["clicked!", "clicked!"]
def test_deprecated_element(self):
self.pyscript_run(
"""
<py-button label="my button">
import js
def on_click(evt):
js.console.log('clicked!')
</py-button>
"""
)
banner = self.page.locator(".py-warning")
banner_content = banner.inner_text()
expected = (
"The element <py-button> is deprecated, create a function with your "
'inline code and use <button py-click="function()" class="py-button"> instead.'
)
assert banner_content == expected
def test_creates_single_deprecation_banner(self):
self.pyscript_run(
"""
<py-button label="my button">
import js
def on_click(evt):
js.console.log('clicked!')
</py-button>
<py-button label="another button!">
</py-button>
"""
)
banner = self.page.query_selector_all(".py-warning")
assert len(banner) == 1
banner_content = banner[0].inner_text()
expected = (
"The element <py-button> is deprecated, create a function with your "
'inline code and use <button py-click="function()" class="py-button"> instead.'
)
assert banner_content == expected

View File

@@ -1,42 +0,0 @@
from .support import PyScriptTest
class TestPyInputBox(PyScriptTest):
def test_input_box_typing(self):
self.pyscript_run(
"""
<py-inputbox label="my input">
import js
def on_keypress(evt):
if evt.key == "Enter":
js.console.log(evt.target.value)
</py-inputbox>
"""
)
assert self.console.log.lines[0] == self.PY_COMPLETE
input = self.page.locator("input")
input.type("Hello")
input.press("Enter")
assert self.console.log.lines[-1] == "Hello"
def test_deprecated_element(self):
self.pyscript_run(
"""
<py-inputbox label="my input">
import js
def on_keypress(evt):
if evt.key == "Enter":
js.console.log(evt.target.value)
</py-inputbox>
"""
)
banner = self.page.locator(".py-warning")
banner_content = banner.inner_text()
expected = (
"The element <py-input> is deprecated, "
'use <input class="py-input"> instead.'
)
assert banner_content == expected

View File

@@ -1,30 +0,0 @@
from .support import PyScriptTest
class TestPyTitle(PyScriptTest):
def test_title_shows_on_page(self):
self.pyscript_run(
"""
<py-title>Hello, World!</py-title>
"""
)
py_title = self.page.query_selector("py-title")
# check that we do have py-title in the page, if not
# py_title will be none
assert py_title
assert py_title.text_content() == "Hello, World!"
def test_deprecated_element(self):
self.pyscript_run(
"""
<py-title>Hello, world!</py-title>
"""
)
banner = self.page.locator(".py-warning")
banner_content = banner.inner_text()
expected = (
"The element <py-title> is deprecated, please use an <h1> tag instead."
)
assert banner_content == expected

View File

@@ -15,10 +15,6 @@ class TestStyle(PyScriptTest):
<py-config>hello</py-config>
<py-script>hello</py-script>
<py-repl>hello</py-repl>
<py-title>hello</py-title>
<py-inputbox>hello</py-inputbox>
<py-button>hello</py-button>
<py-box>hello</py-box>
</body>
</html>
"""
@@ -27,10 +23,6 @@ class TestStyle(PyScriptTest):
expect(self.page.locator("py-config")).to_be_hidden()
expect(self.page.locator("py-script")).to_be_hidden()
expect(self.page.locator("py-repl")).to_be_hidden()
expect(self.page.locator("py-title")).to_be_hidden()
expect(self.page.locator("py-inputbox")).to_be_hidden()
expect(self.page.locator("py-button")).to_be_hidden()
expect(self.page.locator("py-box")).to_be_hidden()
def test_pyscript_defined(self):
"""Test elements have visibility that should"""
@@ -41,26 +33,8 @@ class TestStyle(PyScriptTest):
</py-config>
<py-script>display("hello")</py-script>
<py-repl>display("hello")</py-repl>
<py-title>hello</py-title>
<py-inputbox label="my input">
import js
def on_keypress(evt):
if evt.key == "Enter":
js.console.log(evt.target.value)
</py-inputbox>
<py-box>
<py-button label="my button">
import js
def on_click(evt):
js.console.log('clicked!')
</py-button>
</py-box>
"""
)
expect(self.page.locator("py-config")).to_be_hidden()
expect(self.page.locator("py-script")).to_be_visible()
expect(self.page.locator("py-repl")).to_be_visible()
expect(self.page.locator("py-title")).to_be_visible()
expect(self.page.locator("py-inputbox")).to_be_visible()
expect(self.page.locator("py-button")).to_be_visible()
expect(self.page.locator("py-box")).to_be_visible()

View File

@@ -1,23 +0,0 @@
import { PyBox } from "../../src/components/pybox"
customElements.define('py-box', PyBox)
describe('PyBox', () => {
let instance: PyBox;
beforeEach(() => {
instance = new PyBox();
})
it('PyBox instantiates correctly', async () => {
expect(instance).toBeInstanceOf(PyBox)
})
it("test connectedCallback creates pybox div", async () => {
expect(instance.innerHTML).toBe("")
instance.connectedCallback()
expect(instance.innerHTML).toBe('<div class=\"py-box\"></div>')
})
})

View File

@@ -1,88 +0,0 @@
import type { Runtime } from "../../src/runtime"
import { FakeRuntime } from "./fakeruntime"
import { make_PyButton } from '../../src/components/pybutton';
import { ensureUniqueId } from '../../src/utils';
const runtime: Runtime = new FakeRuntime();
const PyButton = make_PyButton(runtime);
customElements.define('py-button', PyButton);
describe('PyButton', () => {
let instance;
beforeEach(() => {
instance = new PyButton();
// Remove all the alert banners created when calling `connectedCallback`
const banners = document.getElementsByClassName("alert-banner")
for (const banner of banners) {
banner.remove()
}
});
it('should get the Button to just instantiate', async () => {
expect(instance).toBeInstanceOf(PyButton);
});
it('onCallback gets or sets a new id', async () => {
expect(instance.id).toBe('');
instance.connectedCallback();
const instanceId = instance.id;
// id should be similar to py-4850c8c3-d70d-d9e0-03c1-3cfeb0bcec0d-container
expect(instanceId).toMatch(/py-(\w+-){1,5}container/);
// ensureUniqueId doesn't change the ID
ensureUniqueId(instance);
expect(instance.id).toEqual(instanceId);
});
it('onCallback updates on_click code and function name ', async () => {
expect(instance.code).toBe(undefined);
expect(instance.innerHTML).toBe('');
instance.innerHTML = '\ndef on_click(e):\n';
instance.connectedCallback();
expect(instance.code).toMatch(/def\son_click_py_(\w+)\(e\)/);
expect(instance.innerHTML).toContain('<button class="py-button"');
});
it('onCallback updates on_focus code and function name', async () => {
expect(instance.code).toBe(undefined);
expect(instance.innerHTML).toBe('');
instance.innerHTML = '\ndef on_focus(e):\n';
instance.connectedCallback();
expect(instance.code).toMatch(/def\son_focus_py_(\w+)\(e\)/);
expect(instance.innerHTML).toContain('<button class="py-button"');
});
it('onCallback sets mount_name based on id', async () => {
expect(instance.id).toBe('');
expect(instance.mount_name).toBe(undefined);
instance.connectedCallback();
const instanceId = instance.id;
expect(instanceId).toMatch(/py-(\w+-){1,5}container/);
expect(instance.mount_name).toBe(instanceId.replace('-container', '').split('-').join('_'));
});
it('should create a single deprecation banner', async () => {
document.body.innerHTML = ""
let alertBanners = document.getElementsByClassName('alert-banner');
expect(alertBanners.length).toBe(0);
instance.connectedCallback();
expect(alertBanners.length).toBe(1);
expect(alertBanners[0].innerHTML).toContain("&lt;py-button&gt; is deprecated");
// Calling `connectedCallback` again should not create a new banner
instance.connectedCallback();
alertBanners = document.getElementsByClassName('alert-banner');
expect(alertBanners.length).toBe(1);
})
});

View File

@@ -1,62 +0,0 @@
import { jest } from "@jest/globals"
import type { Runtime } from "../../src/runtime"
import { FakeRuntime } from "./fakeruntime"
import { make_PyInputBox } from "../../src/components/pyinputbox"
import { ensureUniqueId } from '../../src/utils';
const runtime: Runtime = new FakeRuntime();
const PyInputBox = make_PyInputBox(runtime);
customElements.define('py-inputbox', PyInputBox)
describe("PyInputBox", () => {
let instance;
beforeEach(() => {
instance = new PyInputBox()
instance.runAfterRuntimeInitialized = jest.fn();
})
it("PyInputBox instantiates correctly", async () => {
expect(instance).toBeInstanceOf(PyInputBox)
})
it('connectedCallback gets or sets a new id', async () => {
expect(instance.id).toBe('');
instance.connectedCallback();
const instanceId = instance.id;
// id should be similar to py-4850c8c3-d70d-d9e0-03c1-3cfeb0bcec0d-container
expect(instanceId).toMatch(/py-(\w+-){1,5}container/);
// ensureUniqueId doesn't change the ID
ensureUniqueId(instance);
expect(instance.id).toEqual(instanceId);
});
it('onCallback sets mount_name based on id', async () => {
expect(instance.id).toBe('');
expect(instance.mount_name).toBe(undefined);
instance.connectedCallback();
const instanceId = instance.id;
expect(instanceId).toMatch(/py-(\w+-){1,5}container/);
expect(instance.mount_name).toBe(instanceId.replace('-container', '').split('-').join('_'));
});
it('onCallback updates on_keypress code and function name ', async () => {
expect(instance.code).toBe(undefined);
expect(instance.innerHTML).toBe('');
instance.innerHTML = '\ndef on_keypress(e):\n';
instance.connectedCallback();
expect(instance.code).toMatch(/def\son_keypress_py_(\w+)\(e\)/);
expect(instance.innerHTML).toContain('<input type="text" class="py-input"');
});
})

View File

@@ -1,35 +0,0 @@
import { PyTitle } from "../../src/components/pytitle"
customElements.define("py-title", PyTitle);
describe("PyTitle", () => {
let instance: PyTitle;
beforeEach(() => {
instance = new PyTitle();
})
it("PyTitle instantiates correctly", async () => {
expect(instance).toBeInstanceOf(PyTitle);
})
it("test connectedCallback defaults", async () => {
instance.connectedCallback();
expect(instance.label).toBe("")
expect(instance.mount_name).toBe("")
expect(instance.innerHTML).toBe(`<div class=\"py-title\" id=\"\"><h1></h1></div>`)
})
it("label renders correctly on the page and updates id", async () => {
instance.innerHTML = "Hello, world!";
instance.id = "my-fancy-title";
instance.connectedCallback();
expect(instance.label).toBe("Hello, world!")
expect(instance.mount_name).toMatch("my_fancy_title");
expect(instance.innerHTML).toContain("<h1>Hello, world!</h1>")
})
})