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

@@ -14,3 +14,9 @@ Documentation
-------------
- Fixed 'Direct usage of document is deprecated' warning in the getting started guide. ([#1052](https://github.com/pyscript/pyscript/pull/1052))
Deprecations and Removals
-------------------------
- The attributes `pys-onClick` and `pys-onKeyDown` have been deprecated, but the warning was only shown in the console. An alert banner will now be shown on the page if the attributes are used. They will be removed in the next release. ([#1084](https://github.com/pyscript/pyscript/pull/1084))
- The pyscript elements `py-button`, `py-inputbox`, `py-box` and `py-title` have now completed their deprecation cycle and have been removed. ([#1084](https://github.com/pyscript/pyscript/pull/1084))

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