Compare commits

...

64 Commits

Author SHA1 Message Date
Andrea Giammarchi
0d0ea96435 Defaulting to async for top-level await (#2134) 2024-08-05 15:55:53 +02:00
Andrea Giammarchi
fafdf74007 Fixed async methods attached to window (#2136) 2024-08-05 11:58:22 +02:00
Martin
999897df12 The all-new, pyscript.web (ignore the branch name :) ) (#2129)
* Minor cleanups: move all Element classes to bottom of module.

* Commenting.

* Commenting.

* Commenting.

* Group dunder methods.

* Don't cache the element's parent.

* Remove style type check until we decide whether or not to add for classes too.

* Add ability to register/unregister element classes.

* Implement __iter__ for container elements.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Minor renaming to make it clear when we have an Element instance vs an actual DOM element.

* remove duplication: added Element.get_tag_name

* Commenting.

* Allow Element.append to 1) use *args, 2) accept iterables

* Remove iterable check - inteferes with js proxies.

* Don't use *args, so it quacks more like a list ;)

* Element.append take 2 :)

* Remove unused code.

* Move to web.py with a page object!

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Added 'page.title' too :)

* Add __getitem__ as a shortcut for page.find

* Add Element.__getitem__ to be consistent

* Make __getitem__ consistent for Page, Element and ElementCollection.

* Docstringing.

* Docstringing.

* Docstringing/commenting.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix select.add (revert InnerHTML->html)

* Commenting.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Hand-edit some of the AI :)

* Rename ElementCollection.children -> ElementCollection.elements

* Remove unnecessary guard.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-08-01 10:36:57 +01:00
Andrea Giammarchi
d47fb58ede Update Pyodide to its 0.26.2 version (#2133) 2024-07-31 20:34:21 +02:00
Andrea Giammarchi
f316341e73 Updated Polyscript and added Panel worker test (#2130)
* Updated Polyscript and added Panel worker test

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-07-31 14:31:35 +02:00
Andrea Giammarchi
8c46fcabf7 Updating polyscript to its latest (#2128)
* Updating polyscript to its latest
2024-07-29 16:59:31 +02:00
Martin
e4ff4d8fab Controversial version where Element just delegates to the underlying DOM element. (#2127)
* Update elements.py

* remove grid which allows simpler class tag mapping.
2024-07-24 13:27:12 -05:00
Martin
f20a0003ed fix: broken methods video.snap and canvas.download (#2126)
* fix: broken methods video.snap and canvas.download

* Allow canvas.draw to use actual image width/height.
2024-07-23 15:35:07 -05:00
Martin
6c938dfe3b Override __getattr__ and __setattr__ on ElementCollection. (#2116)
* Override __getattr__ and __setattr__ on ElementCollection.

* fix: bug when using a string to query an ElementCollection.

* Use Element.find when indexing ElementCollection with a string.

* For consistency also have a find method on ElementCollection.

* ElementCollection.find now returns a collection of collections :)

* fix tests: for textContent

* Revert to extend for ElementCollection.find :)

* Make element_from_dom a classmethod Element.from_dom

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* rename: Element.from_dom -> Element.from_dom_element

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* PyCharm warning sweep.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Workaround for mp not allowing setting via __dict__

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-07-22 10:32:37 -05:00
Andrea Giammarchi
d884586a82 Updated Polyscript to its latest (#2124) 2024-07-19 12:21:04 +02:00
Fabio Pliger
f8f7ba89c1 Cleanup pyscript web elements (#2094)
* change pydom example to use new pyscript.web namespace

* change tests to use new pyscript.web namespace

* create new pyscript.web package and move pydom to pyscript.web.dom

* add __init__ to pyscript.web and expose the dom instance instead of the pyscript.web.dom module

* move elements from pyweb.ui to pyscript.web and temp fix pydom import

* moved of elements file completed

* moved media from pyweb to pyscript.web

* RIP pyweb

* move JSProperty from pyscript.web.dom to pyscript.web.elements

* move element classes from pyscript.web.dom to pyscript.web.elements

* first round of fixes while running tests

* fix test typo

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* restore right type type returned for Element.parent. ALL TESTS PASS LOCALLY NOW

* lint

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* clean up dom.py from dead commented code and osbolete comments

* bugfix: dom shouldn't return None when it can't find any element for a specific selector so it now returns an empty collection

* additional cleanup in tests

* lint

* initial cleaning up of unused modules

* change element.append to not consider types anymore and add tests for appending elements.Element or a JsProxy object

* add Element.append tests for append JS elements directly and appending nodeList as well

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Tag and create the correct subclass of Element.

* Move: Element.snap -> video.snap

* Move: Element.download and draw to canvas.download and draw.

* Minor cleanups.

* Commenting.

* Allow css classes to be passed to Element constructor.

* Commenting.

* Typo fix.

* Make html, id and text JSProperties.

* Commenting.

* Remove unnecessary selected attribute on BaseElement.

* Extract: BaseElement.from_js -> element_from_js

* Pass *args and **kwargs to element_from_js and remove BaseElement.create

* Move value attribute to specific Element subclasses.

* fix: wrapping of existing js elements.

* Add body and head elements so parent and children work everywhere.

* Revert order of HasOptions mixin for the select element.

* Comment out tests that are no longer relevant (see comment).

* Use correct super args in mixin.

* Have to use element_from_js when returning options from OptionsProxy.

* rename: StyleProxy -> Style, OptionsProxy -> Options and added Classes.

* Remove cached_property.

* Remove list-y methods from Classes collection.

* Allow explicit children or *args for containers.

* controversial: fix tests to use find rather than dom

* Add html element so (say) body.parent does what is expected.

* Collapse Element class hierarchy.

* rename: js_element -> dom_element

* rename: element_from_js -> element_from_dom

* replace: JS with DOM

* rename: _js -> _dom_element

* fix dom tests.

* Complete element list with void elements derived from Element.

* Added attributes to the newly added Element subclasses.

* remove dom module, replace with instance.

Also, remove media :)

* fix: typo in test for 'b' element.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Remove dom and media modules.

* fix up ts definitions.

* Added missing import (used in content property).

* Added TODO :)

* wip: Add ClassesCollection

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Attempt to ask black to leave class list alone.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Add classes attribute to ElementCollection

* wip: work on classes collection

* Extract code to get set of all class names in ClassesCollection.

* Update elements.py

* Polishing.

* Make children return an ElementCollection

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* wip: Add the ability to set multiple properties.

* Add __getitem__ back to the dom object.

* Put validation when setting DOM properties back in.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* All tests green.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Remove unnecessary comment.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Martin <martin.chilvers@gmail.com>
2024-07-03 14:21:23 -07:00
Andrea Giammarchi
67d47511d5 Fix MicroPython terminal input when no REPL is used/needed (#2113)
* Fix terminal input when no REPL is used/needed
* Fix input backspace too
2024-07-03 13:03:31 +02:00
Andrea Giammarchi
6f49f18937 Updated Polyscript with its workers feature (#2104)
* Updated Polyscript with its workers feature
* Worked around the inconsistent behavior between Pyodide and MicroPython
* Fixed Pyodide greedy access to undesired Proxy fields
2024-06-26 14:01:22 +02:00
Andrea Giammarchi
7b8ef7ebe2 Fix #2109 - Allow inline JSON config attribute in PyEditor (#2110)
Fix #2109 - Allow inline JSON config attribute in PyEditor
2024-06-24 17:04:28 +02:00
Andrea Giammarchi
461ae38763 Updated reference code to grab latest (#2107) 2024-06-21 16:06:18 +02:00
Andrea Giammarchi
4b90ebdef5 Bring back pyweb as it was (#2105) 2024-06-21 14:49:20 +02:00
Andrea Giammarchi
15c19aa708 Updated Polyscript with latest MicroPython (#2103) 2024-06-19 17:56:22 +02:00
Andrea Giammarchi
d0406be84c A persistent IndexedDB store for PyScript (#2101)
A persistent IndexedDB store for PyScript
2024-06-19 14:11:57 +02:00
Andrea Giammarchi
aab015b9b8 Better py editor indentation (#2098)
Better PyEditor Indentation
2024-06-13 11:34:14 +02:00
Andrea Giammarchi
a1e5a05b49 PyEditor cumulative fixes & improvements (#2095)
* PyEditor fixes

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* PyEditor cumulative fixes & improvements

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-06-12 18:55:36 +02:00
Fabio Pliger
f1a787e031 move pydom and elements from pyweb to pyscript.web (#2092)
* change pydom example to use new pyscript.web namespace

* change tests to use new pyscript.web namespace

* create new pyscript.web package and move pydom to pyscript.web.dom

* add __init__ to pyscript.web and expose the dom instance instead of the pyscript.web.dom module

* move elements from pyweb.ui to pyscript.web and temp fix pydom import

* moved of elements file completed

* moved media from pyweb to pyscript.web

* RIP pyweb

* move JSProperty from pyscript.web.dom to pyscript.web.elements

* move element classes from pyscript.web.dom to pyscript.web.elements

* first round of fixes while running tests

* fix test typo

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* restore right type type returned for Element.parent. ALL TESTS PASS LOCALLY NOW

* lint

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* clean up dom.py from dead commented code and osbolete comments

* bugfix: dom shouldn't return None when it can't find any element for a specific selector so it now returns an empty collection

* additional cleanup in tests

* lint

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-06-06 15:42:14 +02:00
Fabio Pliger
b41cfb7b60 UI creation API (#1960)
* add JSProperty to pydom so it can be used to create descriptors mapping to specific JS attributes

* add pyweb.ui

* fix pyweb imports

* fix link and a elements and add a script element

* fix imports and add initialization to load resources to shoelace module

* new pyweb.ui test folder

* remove comments

* add Icon to shoelace components

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* use html property rather than accessing _js directly

* add markdown suppport

* move examples section out of stdlib pyweb to examples.py in the demo itself

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* simplify demo code

* improve docstrings

* precommit fixes

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* simplify code for main page

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* add load_resources to markdown

* add showlace extra style dynamically

* precommit

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* add gallery files

* add global attributes and dynamic property assignment

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Add shoelace radio component (#1961)

* add shoelace radio component

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

* fix type that lead using the the JSDescriptor directly instead of the factory method

* add missing marked dependency

* refactor gallary to simplify codebase

* precommig lint

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* add text attribute to pydom Elements

* add global JS attributes to elements and improve demos

* lint

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix image instantiation on card since the API has changed

* add attributes to all classes in elements

* lint

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* change example creation functions to take the label and the object directly

* fix input name clashing with input keyword

* refactor examples to better simplify and automate

* fix div clashing names

* fix demo left menu width

* simplify base elements demo by moving all the examples to the examples module

* rename Grid to grid to align to other elements

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* reorg classes order in elements.py and add missing elements to examples

* fix issue related to now importing div from pyweb.ui

* improve demo

* link and fix spelling typo

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Add a bunch more elements (#1966)

* Add copy button

* Add skeleton and spinner

* Add Switch

* Add text area

* Add more elements

* More styling to each component

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Fabio Pliger <fpliger@users.noreply.github.com>

* Add radio group (#1963)

* add radio group

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Fabio Pliger <fpliger@users.noreply.github.com>

* Small tweaks to main demo page (#1962)

* Small tweaks to main demo page

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Fabio Pliger <fpliger@users.noreply.github.com>

* fix post merge issues

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fixed issues with renaming Grid to grid, after we merged

* lint

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* add multple HTML elements in alphabetical order from abbr to em

* fix attributes of some of the elements added in the previous commit and add embed

* fix embed attributes and add fieldset

* add figcation, figure and form. Also fix ordering of definitoin of img and input_

* add style and lint

* wrap up adding all major html elements

* fix test failing due to different error message from fake server compared to a real test server

* change default docstring associated with all classes dynamically patched

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* lint

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* add pyweb tests

* lint

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* add global attributes and change abbr test

* fix abbr to inherit from TextElementBase

* add address test and improve error messaging when ElementBase gets a bad input as style

* change test helper function to be more  flexible on attributes and manage content vs non content based elements. Also adds area tests

* add test for more elements up to caption

* fix canvas and caption as elements that have content and fix name typo on figcaption

* fix another typo on figcaption

* minor fixes and complete tests for all elements

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* adapt shoelace to latest upates in ui.elements

* fix issue with example code not showing created button

* move global attributes to base class

* replace all the calls to _add_js_properties with defining attributes directly in the classes

* finish moving all properties manually on each class

* remove _add_js_properties

* replace JSProperty with js_property

* replace JSProperty with js_property

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fixed merge conflicts

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* remove js_property and just use JSProperty with name and allow_nones as arguments

* fix bug around Element not being able to map global attributes in subclasses

* remove js_property and fix references

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* precommit

* precommit again

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* lint

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* enable pyweb on micropython

* switch examples to micropython

* fix pydom issue with micropython, created by the monkeypatch around JsProxy

* print micropython version on micropython pydom example

* lint and remove of textwrap in stdlib for micropython compatibility

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* added msising attributes on the option Element. Tests are all passing now

* fix tests

* lint

* temp ugly fix for micropython, using the when decorator with a function without arguments

* small fixes and improvements to examples

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix examples and broken link from the removal of markdown and shoelace from stdlib

* lint

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* dynamically change the server address in tests

* use the right element in test_a

* lint

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* skipping test_audio in CI due to different behavior of fake_server vs a real server, that runs in local tests

* add conditional expected_missing_file_errors property to manage different behaviour between local tests and CI (due to fake_server)

* lint

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Askat <aaskat@users.noreply.github.com>
Co-authored-by: Fábio Rosado <hello@fabiorosado.dev>
Co-authored-by: Andrea Giammarchi <andrea.giammarchi@gmail.com>
2024-06-04 13:30:34 -07:00
Andrea Giammarchi
1c675307e1 Expose pyscript.py_import and js_import for lazy Python/JS modules (#2091)
* Expose pyscript.py_modules for lazy Python modules

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-06-04 19:08:52 +02:00
pre-commit-ci[bot]
ac56f82c6d [pre-commit.ci] pre-commit autoupdate (#2089)
updates:
- [github.com/codespell-project/codespell: v2.2.6 → v2.3.0](https://github.com/codespell-project/codespell/compare/v2.2.6...v2.3.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-06-04 09:36:04 +02:00
Andrea Giammarchi
2ac5ca79d7 Expose py-editor code content read/write (#2087)
* Expose py-editor code content read/write

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-06-03 12:32:54 +02:00
Jeff Glass
cb9ee6f7e2 Update README.md, 2023.11.2 -> 2024.5.2 (#2085) 2024-06-03 10:14:27 +02:00
Andrea Giammarchi
9abaef33bd Fix #2067 - Enable .whl packages (#2084) 2024-05-31 11:50:30 +02:00
Brendan
320a537db2 Add step in build process to publish a tarfile (#2077)
Co-authored-by: Andrea Giammarchi <andrea.giammarchi@gmail.com>
2024-05-30 16:25:58 +02:00
Andrea Giammarchi
9b775ce015 Enhance MicroPython Terminal on both Main and Worker (#2083)
* Allow MicroPython Terminal on Main

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-05-30 13:36:42 +02:00
jdw170000
66f72eda1e Add spinner to py-editor run buttons (#2078)
* Add spinner to disabled py-editor run buttons

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* css nit suggested by WebReflection

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-05-29 17:36:06 +02:00
Martin
39ca29749c fix: typo in "isinstance" in Element.snap. (#2071)
Co-authored-by: Fabio Pliger <fpliger@users.noreply.github.com>
Co-authored-by: Andrea Giammarchi <andrea.giammarchi@gmail.com>
2024-05-27 12:08:49 +02:00
Amjith Ramanujam
85da548447 Fix the links to development process. (#2064)
Co-authored-by: Andrea Giammarchi <andrea.giammarchi@gmail.com>
2024-05-27 11:47:25 +02:00
jdw170000
9985787e4b Update dependencies and deprecated linter configuration format (#2076)
* update python dependencies to latest versions

* isort automatic formatting nits

* update eslint config to non-deprecated flat format

* `npm update` to upgrade javascript dependencies

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-05-27 11:44:24 +02:00
Takanori Suzuki
18ec6ce775 refs #2068 fix links on CONTRIBUTING.md (#2072) 2024-05-21 10:36:41 +01:00
Andrea Giammarchi
ed6d0136b8 Updated MicroPython to its latest (#2063)
* Updated MicroPython to its latest

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-05-19 12:10:15 +02:00
Nicholas Tollervey
e7216d26e7 Add release information to README. (#2059)
* Add release information to README.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-05-13 12:27:24 +01:00
Andrea Giammarchi
d1a0d8ea98 Fix #2056 - Provide a default empty config per editor env (#2058)
* Fix #2056 - Provide a default empty config per editor env

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-05-13 10:36:00 +02:00
Andrea Giammarchi
04222b0d03 PyEditor - process(code) ability (#2053)
* Even better PyEditor offline use case (#2050)

* Even better PyEditor offline use case

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

* PyEditor - process(code) ability

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-05-08 21:33:07 +02:00
Andrea Giammarchi
8ec3381789 Even better PyEditor offline use case (#2050)
* Even better PyEditor offline use case

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-05-07 10:02:55 +02:00
pre-commit-ci[bot]
9bd4737708 [pre-commit.ci] pre-commit autoupdate (#2051)
updates:
- [github.com/pre-commit/pre-commit-hooks: v4.5.0 → v4.6.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.5.0...v4.6.0)
- [github.com/psf/black: 24.3.0 → 24.4.2](https://github.com/psf/black/compare/24.3.0...24.4.2)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-05-07 09:42:28 +02:00
Andrea Giammarchi
c49cb9231b Added listeners to the constructing kw options (#2044) 2024-05-03 16:34:21 +02:00
Andrea Giammarchi
d1d1c5740f Fixed py-editor offline use case (#2043) 2024-05-03 12:23:57 +02:00
Andrea Giammarchi
1a05ea5fd2 Fix #2031 - Add pyscript.WebSocket to the mix (#2042)
* Fix #2031 - Add pyscript.WebSocket to the mix

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Working on a test case anyone can run

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-05-03 11:35:05 +02:00
Andrea Giammarchi
5b4e8527da Fix #2040 - Polyscript update to provide config dictionary (#2041)
* Fix #2040 - Polyscript update to provide config dictionary

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-05-02 11:47:52 +02:00
Andrea Giammarchi
83c2afeaf1 Updated Polyscript to its latest (#2036) 2024-04-24 16:56:47 +02:00
Andrea Giammarchi
643b76479f HTML class in MicroPython (#2033) 2024-04-22 12:43:17 +02:00
Andrea Giammarchi
cf92996071 Avoid PyWeb as part of stdlib on MicroPython (#2030)
* Avoid PyWeb as part of stdlib on MicroPython

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-04-17 17:10:23 +02:00
Andrea Giammarchi
c653296821 Added @xterm/addon-web-links to the terminal (#2027)
* Added @xterm/addon-web-links to the terminal

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-04-12 16:25:12 +02:00
Andrea Giammarchi
44cd6273ba PyTerminal .process(code) utility (#2026)
* PyTerminal .process(code) utility

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-04-12 15:44:20 +02:00
pre-commit-ci[bot]
d7d2dfb383 [pre-commit.ci] pre-commit autoupdate (#2012)
updates:
- [github.com/psf/black: 24.1.1 → 24.3.0](https://github.com/psf/black/compare/24.1.1...24.3.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Andrea Giammarchi <andrea.giammarchi@gmail.com>
2024-04-10 16:22:15 +02:00
Andrea Giammarchi
2d5cf096e0 Fix MicroPython badly handling unicode chars (#2018)
## Changes

  * fixed an issue with the **py-editor** related to the new `linebuffer` directive
  * provide in worker hook scope a simple callback that pre-buffers unicode sequences [accordingly to the standard](https://encoding.spec.whatwg.org/#utf-8-bytes-needed) so that the buffer is sent to the terminal only once those sequences are fulfilled
  * test with both `µ` and way more convoluted sequences such as 👩‍❤️‍👨 that the output, if either requested as input or already evaluated from the page works ... in latter case `test = "👩‍❤️‍👨"` completely messes up the program and the resulting string is empty
2024-04-09 14:51:10 +02:00
Andrea Giammarchi
6ee8217593 Updated dev-dependencies w/ ESLint 9 (#2021) 2024-04-09 14:27:06 +02:00
dependabot[bot]
6d45728787 Bump dorny/test-reporter from 1.8.0 to 1.9.0 in the github-actions group (#2019)
Bumps the github-actions group with 1 update: [dorny/test-reporter](https://github.com/dorny/test-reporter).


Updates `dorny/test-reporter` from 1.8.0 to 1.9.0
- [Release notes](https://github.com/dorny/test-reporter/releases)
- [Changelog](https://github.com/dorny/test-reporter/blob/main/CHANGELOG.md)
- [Commits](https://github.com/dorny/test-reporter/compare/v1.8.0...v1.9.0)

---
updated-dependencies:
- dependency-name: dorny/test-reporter
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-08 10:40:53 +02:00
Andrea Giammarchi
65954a627e Allow MicroPython input("...") to work beside the code.interact() (#2017) 2024-04-04 18:13:00 +02:00
Andrea Giammarchi
2f1b764251 Provide input("") and sync output to MicroPython (#2016)
Provide input("") and sync output to MicroPython
2024-04-04 17:34:12 +02:00
Andrea Giammarchi
1fb6cddd70 Forgot to update current npm version (#2015) 2024-04-04 15:53:52 +02:00
Andrea Giammarchi
239add4e20 Better MicroPython Terminal (#2014)
* Better MicroPython Terminal + fixed Pyodide astty value
2024-04-04 15:33:41 +02:00
Andrea Giammarchi
4e4ac56729 Fix #1998 - Allow lazy terminal bootstrap + MicroPython terminal
* Fix #1998 - Allow lazy PyTerminal bootstrap

* Fix #1998 - Allow lazy terminal bootstrap / runtime

* Implemented mpy terminal in both main and worker

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-04-03 15:50:20 +02:00
Andrea Giammarchi
1447cb3094 Fix #1997 - Bring pyscript stdlib to the PyEditor (#2010)
* Fix #1997 - Bring pyscript stdlib to the PyEditor
2024-03-28 10:43:26 +01:00
dependabot[bot]
2f3659b676 Bump the github-actions group with 6 updates (#1995)
Bumps the github-actions group with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [actions/setup-node](https://github.com/actions/setup-node) | `3` | `4` |
| [actions/cache](https://github.com/actions/cache) | `3` | `4` |
| [softprops/action-gh-release](https://github.com/softprops/action-gh-release) | `1` | `2` |
| [conda-incubator/setup-miniconda](https://github.com/conda-incubator/setup-miniconda) | `2` | `3` |
| [actions/upload-artifact](https://github.com/actions/upload-artifact) | `3` | `4` |
| [dorny/test-reporter](https://github.com/dorny/test-reporter) | `1.6.0` | `1.8.0` |


Updates `actions/setup-node` from 3 to 4
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v3...v4)

Updates `actions/cache` from 3 to 4
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v3...v4)

Updates `softprops/action-gh-release` from 1 to 2
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/v1...v2)

Updates `conda-incubator/setup-miniconda` from 2 to 3
- [Release notes](https://github.com/conda-incubator/setup-miniconda/releases)
- [Changelog](https://github.com/conda-incubator/setup-miniconda/blob/main/CHANGELOG.md)
- [Commits](https://github.com/conda-incubator/setup-miniconda/compare/v2...v3)

Updates `actions/upload-artifact` from 3 to 4
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v3...v4)

Updates `dorny/test-reporter` from 1.6.0 to 1.8.0
- [Release notes](https://github.com/dorny/test-reporter/releases)
- [Changelog](https://github.com/dorny/test-reporter/blob/main/CHANGELOG.md)
- [Commits](https://github.com/dorny/test-reporter/compare/v1.6.0...v1.8.0)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/cache
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: softprops/action-gh-release
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: conda-incubator/setup-miniconda
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: dorny/test-reporter
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-25 11:00:29 +01:00
Andrea Giammarchi
910c666319 pyscript.ffi - expose most essential utilities (#2005)
pyscript.ffi - expose most essential utilities
2024-03-22 17:41:06 +01:00
Andrea Giammarchi
eee2f64c1d Update Polyscript with its latest untar.gz and unzip abilities (#2004) 2024-03-22 10:42:01 +01:00
Andrea Giammarchi
d080246a0f Update MicroPython to its latest (#2003) 2024-03-21 11:47:47 +01:00
Andrea Giammarchi
98c0f5e50d Fix #2000 - Allow advanced users to deal themselves with responses (#2001)
* Fix #2000 - Allow advanced users to deal themselves with responses

* rolled back the direct utility idea

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Updated to latest MicroPython and latest way to have direct access with fetch

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-03-20 16:40:24 +01:00
103 changed files with 6007 additions and 2233 deletions

View File

@@ -17,12 +17,12 @@ jobs:
uses: actions/checkout@v4
- name: Install node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18.x
- name: Cache node modules
uses: actions/cache@v3
uses: actions/cache@v4
env:
cache-name: cache-node-modules
with:
@@ -48,7 +48,7 @@ jobs:
run: zip -r -q ./build.zip ./dist
- name: Prepare Release
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
with:
draft: true
prerelease: true

View File

@@ -19,12 +19,12 @@ jobs:
uses: actions/checkout@v4
- name: Install node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18.x
- name: Cache node modules
uses: actions/cache@v3
uses: actions/cache@v4
env:
cache-name: cache-node-modules
with:
@@ -46,6 +46,10 @@ jobs:
working-directory: .
run: sed 's#_PATH_#https://pyscript.net/releases/${{ github.ref_name }}/#' ./public/index.html > ./pyscript.core/dist/index.html
- name: Generate release.tar from snapshot and put it in dist/
working-directory: .
run: tar -cvf ../release.tar * && mv ../release.tar .
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:

View File

@@ -23,12 +23,12 @@ jobs:
uses: actions/checkout@v4
- name: Install node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18.x
- name: Cache node modules
uses: actions/cache@v3
uses: actions/cache@v4
env:
cache-name: cache-node-modules
with:

View File

@@ -24,12 +24,12 @@ jobs:
uses: actions/checkout@v4
- name: Install node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18.x
- name: Cache node modules
uses: actions/cache@v3
uses: actions/cache@v4
env:
cache-name: cache-node-modules
with:

View File

@@ -37,12 +37,12 @@ jobs:
run: git log --graph -3
- name: Install node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 20.x
- name: Cache node modules
uses: actions/cache@v3
uses: actions/cache@v4
env:
cache-name: cache-node-modules
with:
@@ -55,7 +55,7 @@ jobs:
${{ runner.os }}-
- name: setup Miniconda
uses: conda-incubator/setup-miniconda@v2
uses: conda-incubator/setup-miniconda@v3
- name: Create and activate virtual environment
run: |
@@ -76,7 +76,7 @@ jobs:
run: |
make test-integration
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
with:
name: pyscript
path: |
@@ -84,7 +84,7 @@ jobs:
if-no-files-found: error
retention-days: 7
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
if: success() || failure()
with:
name: test_results

View File

@@ -8,7 +8,7 @@ jobs:
report:
runs-on: ubuntu-latest-8core
steps:
- uses: dorny/test-reporter@v1.6.0
- uses: dorny/test-reporter@v1.9.0
with:
artifact: test_results
name: Test reports

1
.gitignore vendored
View File

@@ -142,6 +142,7 @@ coverage/
test_results
# @pyscript/core npm artifacts
pyscript.core/test-results/*
pyscript.core/core.*
pyscript.core/dist
pyscript.core/dist.zip

View File

@@ -7,7 +7,7 @@ ci:
default_stages: [commit]
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
rev: v4.6.0
hooks:
- id: check-builtin-literals
- id: check-case-conflict
@@ -25,13 +25,13 @@ repos:
- id: trailing-whitespace
- repo: https://github.com/psf/black
rev: 24.1.1
rev: 24.4.2
hooks:
- id: black
exclude: pyscript\.core/src/stdlib/pyscript/__init__\.py
- repo: https://github.com/codespell-project/codespell
rev: v2.2.6
rev: v2.3.0
hooks:
- id: codespell # See 'pyproject.toml' for args
exclude: \.js\.map$

View File

@@ -1,5 +1,15 @@
# Release Notes
## 2024.05.21
### Features
### Bug fixes
### Enhancements
- `py-editor` run buttons now display a spinner when disabled, which occurs when the editor is running code.
## 2023.05.01
### Features

View File

@@ -59,9 +59,9 @@ If you would like to contribute to PyScript, but you aren't sure where to begin,
## Setting up your local environment and developing
If you would like to contribute to PyScript, you will need to set up a local development environment. The [following instructions](https://pyscript.github.io/docs/latest/development/setting-up-environment.html) will help you get started.
If you would like to contribute to PyScript, you will need to set up a local development environment. The [following instructions](https://docs.pyscript.net/latest/contributing/#set-up-your-development-environment) will help you get started.
You can also read about PyScript's [development process](https://pyscript.github.io/docs/latest/development/developing.html) to learn how to contribute code to PyScript, how to run tests and what's the PR etiquette of the community!
You can also read about PyScript's [development process](https://docs.pyscript.net/latest/developers/) to learn how to contribute code to PyScript, how to run tests and what's the PR etiquette of the community!
## License terms for contributions

View File

@@ -38,11 +38,11 @@ To try PyScript, import the appropriate pyscript files into the `<head>` tag of
<head>
<link
rel="stylesheet"
href="https://pyscript.net/releases/2023.11.2/core.css"
href="https://pyscript.net/releases/2024.6.2/core.css"
/>
<script
type="module"
src="https://pyscript.net/releases/2023.11.2/core.js"
src="https://pyscript.net/releases/2024.6.2/core.js"
></script>
</head>
<body>
@@ -67,10 +67,29 @@ Check out the [official docs](https://docs.pyscript.net/) for more detailed docu
## How to Contribute
Read the [contributing guide](CONTRIBUTING.md) to learn about our development process, reporting bugs and improvements, creating issues and asking questions.
Read the [contributing guide](https://docs.pyscript.net/latest/contributing/) to learn about our development process, reporting bugs and improvements, creating issues and asking questions.
Check out the [developing process](https://pyscript.github.io/docs/latest/contributing) documentation for more information on how to setup your development environment.
Check out the [developing process](https://docs.pyscript.net/latest/developers/) documentation for more information on how to setup your development environment.
## Governance
The [PyScript organization governance](https://github.com/pyscript/governance) is documented in a separate repository.
## Release
To cut a new release of PyScript simply
[add a new release](https://github.com/pyscript/pyscript/releases) while
remembering to write a comprehensive changelog. A [GitHub action](https://github.com/pyscript/pyscript/blob/main/.github/workflows/publish-release.yml)
will kick in and ensure the release is described and deployed to a URL with the
pattern: https://pyscript.net/releases/YYYY.M.v/ (year/month/version - as per
our [CalVer](https://calver.org/) versioning scheme).
Then, the following three separate repositories need updating:
- [Documentation](https://github.com/pyscript/docs) - Change the `version.json`
file in the root of the directory and then `node version-update.js`.
- [Homepage](https://github.com/pyscript/pyscript.net) - Ensure the version
referenced in `index.html` is the latest version.
- [PSDC](https://pyscript.com) - Use discord or Anaconda Slack (if you work at
Anaconda) to let the PSDC team know there's a new version, so they can update
their project templates.

View File

@@ -1,26 +0,0 @@
module.exports = {
env: {
browser: true,
es2021: true,
},
extends: "eslint:recommended",
overrides: [
{
env: {
node: true,
},
files: [".eslintrc.{js,cjs}"],
parserOptions: {
sourceType: "script",
},
},
],
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
ignorePatterns: ["3rd-party"],
rules: {
"no-implicit-globals": ["error"],
},
};

View File

@@ -1,9 +1,11 @@
.eslintrc.cjs
eslint.config.mjs
.pytest_cache/
node_modules/
rollup/
test/
tests/
test-results/
src/stdlib/_pyscript
src/stdlib/pyscript.py
package-lock.json

View File

@@ -0,0 +1,22 @@
import globals from "globals";
import js from "@eslint/js";
export default [
js.configs.recommended,
{
ignores: ["**/3rd-party/"],
},
{
languageOptions: {
ecmaVersion: "latest",
sourceType: "module",
globals: {
...globals.browser,
...globals.es2021,
},
},
rules: {
"no-implicit-globals": ["error"],
},
},
];

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@pyscript/core",
"version": "0.4.6",
"version": "0.5.3",
"type": "module",
"description": "PyScript",
"module": "./index.js",
@@ -20,13 +20,15 @@
},
"scripts": {
"server": "npx static-handler --coi .",
"build": "npm run build:3rd-party && npm run build:stdlib && npm run build:plugins && npm run build:core && eslint src/ && npm run ts && npm run test:mpy",
"build": "export ESLINT_USE_FLAT_CONFIG=true;npm run build:3rd-party && npm run build:stdlib && npm run build:plugins && npm run build:core && if [ -z \"$NO_MIN\" ]; then eslint src/ && npm run ts && npm run test:mpy; fi",
"build:core": "rm -rf dist && rollup --config rollup/core.config.js && cp src/3rd-party/*.css dist/",
"build:flatted": "node rollup/flatted.cjs",
"build:plugins": "node rollup/plugins.cjs",
"build:stdlib": "node rollup/stdlib.cjs",
"build:3rd-party": "node rollup/3rd-party.cjs",
"clean:3rd-party": "rm src/3rd-party/*.js && rm src/3rd-party/*.css",
"test:mpy": "static-handler --coi . 2>/dev/null & SH_PID=$!; EXIT_CODE=0; playwright test --fully-parallel test/ || EXIT_CODE=$?; kill $SH_PID 2>/dev/null; exit $EXIT_CODE",
"test:mpy": "static-handler --coi . 2>/dev/null & SH_PID=$!; EXIT_CODE=0; playwright test --fully-parallel test/mpy.spec.js || EXIT_CODE=$?; kill $SH_PID 2>/dev/null; exit $EXIT_CODE",
"test:ws": "bun test/ws/index.js & playwright test test/ws.spec.js",
"dev": "node dev.cjs",
"release": "npm run build && npm run zip",
"size": "echo -e \"\\033[1mdist/*.js file size\\033[0m\"; for js in $(ls dist/*.js); do cat $js | brotli > ._; echo -e \"\\033[2m$js:\\033[0m $(du -h --apparent-size ._ | sed -e 's/[[:space:]]*._//')\"; rm ._; done",
@@ -42,31 +44,34 @@
"dependencies": {
"@ungap/with-resolvers": "^0.1.0",
"basic-devtools": "^0.1.6",
"polyscript": "^0.11.0",
"polyscript": "^0.15.0",
"sticky-module": "^0.1.1",
"to-json-callback": "^0.1.1",
"type-checked-collections": "^0.1.7"
},
"devDependencies": {
"@codemirror/commands": "^6.3.3",
"@codemirror/lang-python": "^6.1.4",
"@codemirror/language": "^6.10.1",
"@codemirror/commands": "^6.6.0",
"@codemirror/lang-python": "^6.1.6",
"@codemirror/language": "^6.10.2",
"@codemirror/state": "^6.4.1",
"@codemirror/view": "^6.25.1",
"@playwright/test": "^1.42.1",
"@rollup/plugin-commonjs": "^25.0.7",
"@codemirror/view": "^6.30.0",
"@playwright/test": "^1.45.3",
"@rollup/plugin-commonjs": "^26.0.1",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-terser": "^0.4.4",
"@webreflection/toml-j0.4": "^1.1.3",
"@xterm/addon-fit": "^0.9.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-web-links": "^0.11.0",
"bun": "^1.1.21",
"chokidar": "^3.6.0",
"codemirror": "^6.0.1",
"eslint": "^8.57.0",
"rollup": "^4.12.1",
"eslint": "^9.8.0",
"flatted": "^3.3.1",
"rollup": "^4.20.0",
"rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-string": "^3.0.0",
"static-handler": "^0.4.3",
"typescript": "^5.4.2",
"typescript": "^5.5.4",
"xterm": "^5.3.0",
"xterm-readline": "^1.1.1"
},

Binary file not shown.

View File

@@ -51,6 +51,9 @@ const modules = {
"xterm_addon-fit.js": fetch(`${CDN}/@xterm/addon-fit/+esm`).then((b) =>
b.text(),
),
"xterm_addon-web-links.js": fetch(
`${CDN}/@xterm/addon-web-links/+esm`,
).then((b) => b.text()),
"xterm.css": fetch(`${CDN}/xterm@${v("xterm")}/css/xterm.min.css`).then(
(b) => b.text(),
),

View File

@@ -0,0 +1,17 @@
const { writeFileSync, readFileSync } = require("node:fs");
const { join } = require("node:path");
const flatted = "# https://www.npmjs.com/package/flatted\n\n";
const source = join(
__dirname,
"..",
"node_modules",
"flatted",
"python",
"flatted.py",
);
const dest = join(__dirname, "..", "src", "stdlib", "pyscript", "flatted.py");
const clear = (str) => String(str).replace(/^#.*/gm, "").trimStart();
writeFileSync(dest, flatted + clear(readFileSync(source)));

View File

@@ -45,6 +45,8 @@ const configDetails = async (config, type) => {
const conflictError = (reason) => new Error(`(${CONFLICTING_CODE}): ${reason}`);
const relative_url = (url, base = location.href) => new URL(url, base).href;
const syntaxError = (type, url, { message }) => {
let str = `(${BAD_CONFIG}): Invalid ${type}`;
if (url) str += ` @ ${url}`;
@@ -108,7 +110,7 @@ for (const [TYPE] of TYPES) {
if (!error && config) {
try {
const { json, toml, text, url } = await configDetails(config, type);
if (url) configURL = new URL(url, location.href).href;
if (url) configURL = relative_url(url);
config = text;
if (json || type === "json") {
try {
@@ -153,4 +155,4 @@ for (const [TYPE] of TYPES) {
configs.set(TYPE, { config: parsed, configURL, plugins, error });
}
export default configs;
export { configs, relative_url };

View File

@@ -42,3 +42,34 @@ mpy-config {
.mpy-editor-run-button:disabled {
opacity: 1;
}
@keyframes spinner {
to {
transform: rotate(360deg);
}
}
.py-editor-run-button:disabled > *,
.mpy-editor-run-button:disabled > * {
display: none; /* hide all the child elements of the run button when it is disabled */
}
.py-editor-run-button:disabled,
.mpy-editor-run-button:disabled {
border-width: 0;
}
.py-editor-run-button:disabled::before,
.mpy-editor-run-button:disabled::before {
content: "";
box-sizing: border-box;
position: absolute;
top: 100%;
left: 100%;
width: 20px;
height: 20px;
margin-top: -23px; /* hardcoded value to center the spinner on the run button */
margin-left: -26px; /* hardcoded value to center the spinner on the run button */
border-radius: 50%;
border: 2px solid #aaa;
border-top-color: #000;
background-color: #fff;
animation: spinner 0.6s linear infinite;
}

View File

@@ -12,6 +12,7 @@ import {
define,
defineProperty,
dispatch,
isSync,
queryTarget,
unescape,
whenDefined,
@@ -19,12 +20,22 @@ import {
import "./all-done.js";
import TYPES from "./types.js";
import configs from "./config.js";
import { configs, relative_url } from "./config.js";
import sync from "./sync.js";
import bootstrapNodeAndPlugins from "./plugins-helper.js";
import { ErrorCode } from "./exceptions.js";
import { robustFetch as fetch, getText } from "./fetch.js";
import { hooks, main, worker, codeFor, createFunction } from "./hooks.js";
import {
hooks,
main,
worker,
codeFor,
createFunction,
inputFailure,
} from "./hooks.js";
import { stdlib, optional } from "./stdlib.js";
export { stdlib, optional, inputFailure };
// generic helper to disambiguate between custom element and script
const isScript = ({ tagName }) => tagName === "SCRIPT";
@@ -74,6 +85,7 @@ const [
export {
TYPES,
relative_url,
exportedPyWorker as PyWorker,
exportedMPWorker as MPWorker,
exportedHooks as hooks,
@@ -81,6 +93,9 @@ export {
exportedWhenDefined as whenDefined,
};
export const offline_interpreter = (config) =>
config?.interpreter && relative_url(config.interpreter);
const hooked = new Map();
for (const [TYPE, interpreter] of TYPES) {
@@ -144,6 +159,7 @@ for (const [TYPE, interpreter] of TYPES) {
// enrich the Python env with some JS utility for main
interpreter.registerJsModule("_pyscript", {
PyWorker,
js_import: (...urls) => Promise.all(urls.map((url) => import(url))),
get target() {
return isScript(currentElement)
? currentElement.target.id
@@ -165,7 +181,7 @@ for (const [TYPE, interpreter] of TYPES) {
// specific main and worker hooks
const hooks = {
main: {
...codeFor(main),
...codeFor(main, TYPE),
async onReady(wrap, element) {
registerModule(wrap);
@@ -187,15 +203,13 @@ for (const [TYPE, interpreter] of TYPES) {
}
if (isScript(element)) {
const {
attributes: { async: isAsync, target },
} = element;
const hasTarget = !!target?.value;
const show = hasTarget
? queryTarget(element, target.value)
const isAsync = !isSync(element);
const target = element.getAttribute("target");
const show = target
? queryTarget(element, target)
: document.createElement("script-py");
if (!hasTarget) {
if (!target) {
const { head, body } = document;
if (head.contains(element)) body.append(show);
else element.after(show);
@@ -262,7 +276,7 @@ for (const [TYPE, interpreter] of TYPES) {
},
},
worker: {
...codeFor(worker),
...codeFor(worker, TYPE),
// these are lazy getters that returns a composition
// of the current hooks or undefined, if no hook is present
get onReady() {
@@ -291,7 +305,7 @@ for (const [TYPE, interpreter] of TYPES) {
interpreter,
hooks,
env: `${TYPE}-script`,
version: config?.interpreter,
version: offline_interpreter(config),
onerror(error, element) {
errors.set(element, error);
},
@@ -316,7 +330,7 @@ for (const [TYPE, interpreter] of TYPES) {
async connectedCallback() {
if (!this.executed) {
this.executed = true;
const isAsync = this.hasAttribute("async");
const isAsync = !isSync(this);
const { io, run, runAsync } = await this._wrap
.promise;
this.srcCode = await fetchSource(

View File

@@ -2,7 +2,7 @@ import { typedSet } from "type-checked-collections";
import { dedent } from "polyscript/exports";
import toJSONCallback from "to-json-callback";
import stdlib from "./stdlib.js";
import { stdlib, optional } from "./stdlib.js";
export const main = (name) => hooks.main[name];
export const worker = (name) => hooks.worker[name];
@@ -15,10 +15,11 @@ const code = (hooks, branch, key, lib) => {
};
};
export const codeFor = (branch) => {
export const codeFor = (branch, type) => {
const pylib = type === "mpy" ? stdlib.replace(optional, "") : stdlib;
const hooks = {};
code(hooks, branch, `codeBeforeRun`, stdlib);
code(hooks, branch, `codeBeforeRunAsync`, stdlib);
code(hooks, branch, `codeBeforeRun`, pylib);
code(hooks, branch, `codeBeforeRunAsync`, pylib);
code(hooks, branch, `codeAfterRun`);
code(hooks, branch, `codeAfterRunAsync`);
return hooks;
@@ -45,7 +46,7 @@ export const createFunction = (self, name) => {
const SetFunction = typedSet({ typeof: "function" });
const SetString = typedSet({ typeof: "string" });
const inputFailure = `
export const inputFailure = `
import builtins
def input(prompt=""):
raise Exception("\\n ".join([

View File

@@ -1,6 +1,6 @@
// PyScript py-editor plugin
import { Hook, XWorker, dedent } from "polyscript/exports";
import { TYPES } from "../core.js";
import { Hook, XWorker, dedent, defineProperties } from "polyscript/exports";
import { TYPES, offline_interpreter, relative_url, stdlib } from "../core.js";
const RUN_BUTTON = `<svg style="height:20px;width:20px;vertical-align:-.125em;transform-origin:center;overflow:visible;color:green" viewBox="0 0 384 512" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg"><g transform="translate(192 256)" transform-origin="96 0"><g transform="translate(0,0) scale(1,1)"><path d="M361 215C375.3 223.8 384 239.3 384 256C384 272.7 375.3 288.2 361 296.1L73.03 472.1C58.21 482 39.66 482.4 24.52 473.9C9.377 465.4 0 449.4 0 432V80C0 62.64 9.377 46.63 24.52 38.13C39.66 29.64 58.21 29.99 73.03 39.04L361 215z" fill="currentColor" transform="translate(-192 -256)"></path></g></g></svg>`;
@@ -8,13 +8,15 @@ let id = 0;
const getID = (type) => `${type}-editor-${id++}`;
const envs = new Map();
const configs = new Map();
const hooks = {
worker: {
codeBeforeRun: () => stdlib,
// works on both Pyodide and MicroPython
onReady: ({ runAsync, io }, { sync }) => {
io.stdout = (line) => sync.write(line);
io.stderr = (line) => sync.writeErr(line);
io.stdout = io.buffered(sync.write);
io.stderr = io.buffered(sync.writeErr);
sync.revoke();
sync.runAsync = runAsync;
},
@@ -32,9 +34,31 @@ async function execute({ currentTarget }) {
if (!envs.has(env)) {
const srcLink = URL.createObjectURL(new Blob([""]));
const xworker = XWorker.call(new Hook(null, hooks), srcLink, {
const details = {
type: this.interpreter,
});
serviceWorker: this.serviceWorker,
};
const { config } = this;
if (config) {
details.configURL = relative_url(config);
if (config.endsWith(".toml")) {
const [{ parse }, toml] = await Promise.all([
import(/* webpackIgnore: true */ "../3rd-party/toml.js"),
fetch(config).then((r) => r.text()),
]);
details.config = parse(toml);
} else if (config.endsWith(".json")) {
details.config = await fetch(config).then((r) => r.json());
} else {
details.configURL = relative_url("./config.txt");
details.config = JSON.parse(config);
}
details.version = offline_interpreter(details.config);
} else {
details.config = {};
}
const xworker = XWorker.call(new Hook(null, hooks), srcLink, details);
const { sync } = xworker;
const { promise, resolve } = Promise.withResolvers();
@@ -47,7 +71,7 @@ async function execute({ currentTarget }) {
// wait for the env then set the target div
// before executing the current code
envs.get(env).then((xworker) => {
return envs.get(env).then((xworker) => {
xworker.onerror = ({ error }) => {
if (hasRunButton) {
outDiv.innerHTML += `<span style='color:red'>${
@@ -73,21 +97,24 @@ async function execute({ currentTarget }) {
});
}
const makeRunButton = (listener, type) => {
const makeRunButton = (handler, type) => {
const runButton = document.createElement("button");
runButton.className = `absolute ${type}-editor-run-button`;
runButton.innerHTML = RUN_BUTTON;
runButton.setAttribute("aria-label", "Python Script Run Button");
runButton.addEventListener("click", listener);
runButton.addEventListener("click", async (event) => {
runButton.blur();
await handler.handleEvent(event);
});
return runButton;
};
const makeEditorDiv = (listener, type) => {
const makeEditorDiv = (handler, type) => {
const editorDiv = document.createElement("div");
editorDiv.className = `${type}-editor-input`;
editorDiv.setAttribute("aria-label", "Python Script Area");
const runButton = makeRunButton(listener, type);
const runButton = makeRunButton(handler, type);
const editorShadowContainer = document.createElement("div");
// avoid outer elements intercepting key events (reveal as example)
@@ -107,15 +134,15 @@ const makeOutDiv = (type) => {
return outDiv;
};
const makeBoxDiv = (listener, type) => {
const makeBoxDiv = (handler, type) => {
const boxDiv = document.createElement("div");
boxDiv.className = `${type}-editor-box`;
const editorDiv = makeEditorDiv(listener, type);
const editorDiv = makeEditorDiv(handler, type);
const outDiv = makeOutDiv(type);
boxDiv.append(editorDiv, outDiv);
return [boxDiv, outDiv];
return [boxDiv, outDiv, editorDiv.querySelector("button")];
};
const init = async (script, type, interpreter) => {
@@ -125,7 +152,7 @@ const init = async (script, type, interpreter) => {
{ python },
{ indentUnit },
{ keymap },
{ defaultKeymap },
{ defaultKeymap, indentWithTab },
] = await Promise.all([
import(/* webpackIgnore: true */ "../3rd-party/codemirror.js"),
import(/* webpackIgnore: true */ "../3rd-party/codemirror_state.js"),
@@ -137,14 +164,39 @@ const init = async (script, type, interpreter) => {
import(/* webpackIgnore: true */ "../3rd-party/codemirror_commands.js"),
]);
const isSetup = script.hasAttribute("setup");
let isSetup = script.hasAttribute("setup");
const hasConfig = script.hasAttribute("config");
const serviceWorker = script.getAttribute("service-worker");
const env = `${interpreter}-${script.getAttribute("env") || getID(type)}`;
const source = script.src
// helps preventing too lazy ServiceWorker initialization on button run
if (serviceWorker) {
new XWorker("data:application/javascript,postMessage(0)", {
type: "dummy",
serviceWorker,
}).onmessage = ({ target }) => target.terminate();
}
if (hasConfig && configs.has(env)) {
throw new SyntaxError(
configs.get(env)
? `duplicated config for env: ${env}`
: `unable to add a config to the env: ${env}`,
);
}
configs.set(env, hasConfig);
let source = script.src
? await fetch(script.src).then((b) => b.text())
: script.textContent;
const context = {
// allow the listener to be overridden at distance
handleEvent: execute,
serviceWorker,
interpreter,
env,
config: hasConfig && script.getAttribute("config"),
get pySrc() {
return isSetup ? source : editor.state.doc.toString();
},
@@ -153,14 +205,82 @@ const init = async (script, type, interpreter) => {
},
};
let target;
defineProperties(script, {
target: { get: () => target },
handleEvent: {
get: () => context.handleEvent,
set: (callback) => {
// do not bother with logic if it was set back as its original handler
if (callback === execute) context.handleEvent = execute;
// in every other case be sure that if the listener override returned
// `false` nothing happens, otherwise keep doing what it always did
else {
context.handleEvent = async (event) => {
// trap the currentTarget ASAP (if any)
// otherwise it gets lost asynchronously
const { currentTarget } = event;
// augment a code snapshot before invoking the override
defineProperties(event, {
code: { value: context.pySrc },
});
// avoid executing the default handler if the override returned `false`
if ((await callback(event)) !== false)
await execute.call(context, { currentTarget });
};
}
},
},
code: {
get: () => context.pySrc,
set: (insert) => {
if (isSetup) return;
editor.update([
editor.state.update({
changes: {
from: 0,
to: editor.state.doc.length,
insert,
},
}),
]);
},
},
process: {
/**
* Simulate a setup node overriding the source to evaluate.
* @param {string} code the Python code to evaluate.
* @returns {Promise<...>} fulfill once code has been evaluated.
*/
value(code) {
const wasSetup = isSetup;
const wasSource = source;
isSetup = true;
source = code;
const restore = () => {
isSetup = wasSetup;
source = wasSource;
};
return context
.handleEvent({ currentTarget: null })
.then(restore, restore);
},
},
});
const notify = () => {
const event = new Event(`${type}-editor`, { bubbles: true });
script.dispatchEvent(event);
};
if (isSetup) {
execute.call(context, { currentTarget: null });
await context.handleEvent({ currentTarget: null });
notify();
return;
}
const selector = script.getAttribute("target");
let target;
if (selector) {
target =
document.getElementById(selector) ||
@@ -177,8 +297,7 @@ const init = async (script, type, interpreter) => {
if (!target.hasAttribute("root")) target.setAttribute("root", target.id);
// @see https://github.com/JeffersGlass/mkdocs-pyscript/blob/main/mkdocs_pyscript/js/makeblocks.js
const listener = execute.bind(context);
const [boxDiv, outDiv] = makeBoxDiv(listener, type);
const [boxDiv, outDiv, runButton] = makeBoxDiv(context, type);
boxDiv.dataset.env = script.hasAttribute("env") ? env : interpreter;
const inputChild = boxDiv.querySelector(`.${type}-editor-input > div`);
@@ -191,8 +310,9 @@ const init = async (script, type, interpreter) => {
const doc = dedent(script.textContent).trim();
// preserve user indentation, if any
const indentation = /^(\s+)/m.test(doc) ? RegExp.$1 : " ";
const indentation = /^([ \t]+)/m.test(doc) ? RegExp.$1 : " ";
const listener = () => runButton.click();
const editor = new EditorView({
extensions: [
indentUnit.of(indentation),
@@ -202,14 +322,19 @@ const init = async (script, type, interpreter) => {
{ key: "Ctrl-Enter", run: listener, preventDefault: true },
{ key: "Cmd-Enter", run: listener, preventDefault: true },
{ key: "Shift-Enter", run: listener, preventDefault: true },
// @see https://codemirror.net/examples/tab/
indentWithTab,
]),
basicSetup,
],
foldGutter: true,
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
parent,
doc,
});
editor.focus();
notify();
};
// avoid too greedy MutationObserver operations at distance
@@ -225,7 +350,7 @@ const resetTimeout = () => {
};
// triggered both ASAP on the living DOM and via MutationObserver later
const pyEditor = async () => {
const pyEditor = () => {
if (timeout) return;
timeout = setTimeout(resetTimeout, 250);
for (const [type, interpreter] of TYPES) {

View File

@@ -1,11 +1,13 @@
// PyScript py-terminal plugin
import { TYPES, hooks } from "../core.js";
import { TYPES, relative_url } from "../core.js";
import { notify } from "./error.js";
import { defineProperty } from "polyscript/exports";
import { customObserver } from "polyscript/exports";
const SELECTOR = [...TYPES.keys()]
.map((type) => `script[type="${type}"][terminal],${type}-script[terminal]`)
.join(",");
// will contain all valid selectors
const SELECTORS = [];
// avoid processing same elements twice
const processed = new WeakSet();
// show the error on main and
// stops the module from keep executing
@@ -14,193 +16,45 @@ const notifyAndThrow = (message) => {
throw new Error(message);
};
const notParsedYet = (script) => !bootstrapped.has(script);
const onceOnMain = ({ attributes: { worker } }) => !worker;
const bootstrapped = new WeakSet();
let addStyle = true;
// this callback will be serialized as string and it never needs
// to be invoked multiple times. Each xworker here is bootstrapped
// only once thanks to the `sync.is_pyterminal()` check.
const workerReady = ({ interpreter, io, run }, { sync }) => {
if (!sync.is_pyterminal()) return;
for (const type of TYPES.keys()) {
const selector = `script[type="${type}"][terminal],${type}-script[terminal]`;
SELECTORS.push(selector);
customObserver.set(selector, async (element) => {
// we currently support only one terminal on main as in "classic"
const terminals = document.querySelectorAll(SELECTORS.join(","));
if ([].filter.call(terminals, onceOnMain).length > 1)
notifyAndThrow("You can use at most 1 main terminal");
// in workers it's always safe to grab the polyscript currentScript
run("from polyscript.currentScript import terminal as __terminal__");
// This part is inevitably duplicated as external scope
// can't be reached by workers out of the box.
// The detail is that here we use sync though, not readline.
const decoder = new TextDecoder();
let data = "";
const generic = {
isatty: true,
write(buffer) {
data = decoder.decode(buffer);
sync.pyterminal_write(data);
return buffer.length;
},
};
interpreter.setStdout(generic);
interpreter.setStderr(generic);
interpreter.setStdin({
isatty: true,
stdin: () => sync.pyterminal_read(data),
});
io.stderr = (error) => {
sync.pyterminal_write(`${error.message || error}\n`);
};
};
const pyTerminal = async () => {
const terminals = document.querySelectorAll(SELECTOR);
const unknown = [].filter.call(terminals, notParsedYet);
// no results will look further for runtime nodes
if (!unknown.length) return;
// early flag elements as known to avoid concurrent
// MutationObserver invokes of this async handler
else unknown.forEach(bootstrapped.add, bootstrapped);
// we currently support only one terminal as in "classic"
if ([].filter.call(terminals, onceOnMain).length > 1)
notifyAndThrow("You can use at most 1 main terminal");
// import styles lazily
if (addStyle) {
addStyle = false;
document.head.append(
Object.assign(document.createElement("link"), {
rel: "stylesheet",
href: new URL("./xterm.css", import.meta.url),
}),
);
}
// lazy load these only when a valid terminal is found
const [{ Terminal }, { Readline }, { FitAddon }] = await Promise.all([
import(/* webpackIgnore: true */ "../3rd-party/xterm.js"),
import(/* webpackIgnore: true */ "../3rd-party/xterm-readline.js"),
import(/* webpackIgnore: true */ "../3rd-party/xterm_addon-fit.js"),
]);
for (const element of unknown) {
// hopefully to be removed in the near future!
if (element.matches('script[type="mpy"],mpy-script'))
notifyAndThrow("Unsupported terminal.");
const readline = new Readline();
// common main thread initialization for both worker
// or main case, bootstrapping the terminal on its target
const init = (options) => {
let target = element;
const selector = element.getAttribute("target");
if (selector) {
target =
document.getElementById(selector) ||
document.querySelector(selector);
if (!target) throw new Error(`Unknown target ${selector}`);
} else {
target = document.createElement("py-terminal");
target.style.display = "block";
element.after(target);
}
const terminal = new Terminal({
theme: {
background: "#191A19",
foreground: "#F5F2E7",
},
...options,
});
const fitAddon = new FitAddon();
terminal.loadAddon(fitAddon);
terminal.loadAddon(readline);
terminal.open(target);
fitAddon.fit();
terminal.focus();
defineProperty(element, "terminal", { value: terminal });
return terminal;
};
// branch logic for the worker
if (element.hasAttribute("worker")) {
// add a hook on the main thread to setup all sync helpers
// also bootstrapping the XTerm target on main *BUT* ...
hooks.main.onWorker.add(function worker(_, xworker) {
// ... as multiple workers will add multiple callbacks
// be sure no xworker is ever initialized twice!
if (bootstrapped.has(xworker)) return;
bootstrapped.add(xworker);
// still cleanup this callback for future scripts/workers
hooks.main.onWorker.delete(worker);
init({
disableStdin: false,
cursorBlink: true,
cursorStyle: "block",
});
xworker.sync.is_pyterminal = () => true;
xworker.sync.pyterminal_read = readline.read.bind(readline);
xworker.sync.pyterminal_write = readline.write.bind(readline);
});
// setup remote thread JS/Python code for whenever the
// worker is ready to become a terminal
hooks.worker.onReady.add(workerReady);
} else {
// in the main case, just bootstrap XTerm without
// allowing any input as that's not possible / awkward
hooks.main.onReady.add(function main({ interpreter, io, run }) {
console.warn("py-terminal is read only on main thread");
hooks.main.onReady.delete(main);
// on main, it's easy to trash and clean the current terminal
globalThis.__py_terminal__ = init({
disableStdin: true,
cursorBlink: false,
cursorStyle: "underline",
});
run("from js import __py_terminal__ as __terminal__");
delete globalThis.__py_terminal__;
// This part is inevitably duplicated as external scope
// can't be reached by workers out of the box.
// The detail is that here we use readline here, not sync.
const decoder = new TextDecoder();
let data = "";
const generic = {
isatty: true,
write(buffer) {
data = decoder.decode(buffer);
readline.write(data);
return buffer.length;
},
};
interpreter.setStdout(generic);
interpreter.setStderr(generic);
interpreter.setStdin({
isatty: true,
stdin: () => readline.read(data),
});
io.stderr = (error) => {
readline.write(`${error.message || error}\n`);
};
});
// import styles lazily
if (addStyle) {
addStyle = false;
document.head.append(
Object.assign(document.createElement("link"), {
rel: "stylesheet",
href: relative_url("./xterm.css", import.meta.url),
}),
);
}
}
};
const mo = new MutationObserver(pyTerminal);
mo.observe(document, { childList: true, subtree: true });
if (processed.has(element)) return;
processed.add(element);
// try to check the current document ASAP
export default pyTerminal();
const bootstrap = (module) => module.default(element);
// we can't be smart with template literals for the dynamic import
// or bundlers are incapable of producing multiple files around
if (type === "mpy") {
await import(/* webpackIgnore: true */ "./py-terminal/mpy.js").then(
bootstrap,
);
} else {
await import(/* webpackIgnore: true */ "./py-terminal/py.js").then(
bootstrap,
);
}
});
}

View File

@@ -0,0 +1,252 @@
// PyScript pyodide terminal plugin
import { hooks, inputFailure } from "../../core.js";
import { defineProperties } from "polyscript/exports";
const bootstrapped = new WeakSet();
// this callback will be serialized as string and it never needs
// to be invoked multiple times. Each xworker here is bootstrapped
// only once thanks to the `sync.is_pyterminal()` check.
const workerReady = ({ interpreter, io, run, type }, { sync }) => {
if (type !== "mpy" || !sync.is_pyterminal()) return;
const { pyterminal_ready, pyterminal_read, pyterminal_write } = sync;
interpreter.registerJsModule("_pyscript_input", {
input: pyterminal_read,
});
run(
[
"from _pyscript_input import input",
"from polyscript import currentScript as _",
"__terminal__ = _.terminal",
"del _",
].join(";"),
);
const missingReturn = new Uint8Array([13]);
io.stdout = (buffer) => {
if (buffer[0] === 10) pyterminal_write(missingReturn);
pyterminal_write(buffer);
};
io.stderr = (error) => {
pyterminal_write(String(error.message || error));
};
// tiny shim of the code module with only interact
// to bootstrap a REPL like environment
interpreter.registerJsModule("code", {
interact() {
const encoder = new TextEncoderStream();
encoder.readable.pipeTo(
new WritableStream({
write(buffer) {
for (const c of buffer) interpreter.replProcessChar(c);
},
}),
);
const writer = encoder.writable.getWriter();
sync.pyterminal_stream_write = (buffer) => writer.write(buffer);
interpreter.replInit();
},
});
pyterminal_ready();
};
export default async (element) => {
// lazy load these only when a valid terminal is found
const [{ Terminal }, { FitAddon }, { WebLinksAddon }] = await Promise.all([
import(/* webpackIgnore: true */ "../../3rd-party/xterm.js"),
import(/* webpackIgnore: true */ "../../3rd-party/xterm_addon-fit.js"),
import(
/* webpackIgnore: true */ "../../3rd-party/xterm_addon-web-links.js"
),
]);
const terminalOptions = {
disableStdin: false,
cursorBlink: true,
cursorStyle: "block",
};
let stream;
// common main thread initialization for both worker
// or main case, bootstrapping the terminal on its target
const init = () => {
let target = element;
const selector = element.getAttribute("target");
if (selector) {
target =
document.getElementById(selector) ||
document.querySelector(selector);
if (!target) throw new Error(`Unknown target ${selector}`);
} else {
target = document.createElement("py-terminal");
target.style.display = "block";
element.after(target);
}
const terminal = new Terminal({
theme: {
background: "#191A19",
foreground: "#F5F2E7",
},
...terminalOptions,
});
const fitAddon = new FitAddon();
terminal.loadAddon(fitAddon);
terminal.loadAddon(new WebLinksAddon());
terminal.open(target);
fitAddon.fit();
terminal.focus();
defineProperties(element, {
terminal: { value: terminal },
process: {
value: async (code) => {
for (const line of code.split(/(?:\r\n|\r|\n)/)) {
await stream.write(`${line}\r`);
}
},
},
});
return terminal;
};
// branch logic for the worker
if (element.hasAttribute("worker")) {
// add a hook on the main thread to setup all sync helpers
// also bootstrapping the XTerm target on main *BUT* ...
hooks.main.onWorker.add(function worker(_, xworker) {
// ... as multiple workers will add multiple callbacks
// be sure no xworker is ever initialized twice!
if (bootstrapped.has(xworker)) return;
bootstrapped.add(xworker);
// still cleanup this callback for future scripts/workers
hooks.main.onWorker.delete(worker);
const terminal = init();
const { sync } = xworker;
// handle the read mode on input
let promisedChunks = null;
let readChunks = "";
sync.is_pyterminal = () => true;
// put the terminal in a read-only state
// frees the worker on \r
sync.pyterminal_read = (buffer) => {
terminal.write(buffer);
promisedChunks = Promise.withResolvers();
return promisedChunks.promise;
};
// write if not reading input
sync.pyterminal_write = (buffer) => {
if (!promisedChunks) terminal.write(buffer);
};
// add the onData terminal listener which forwards to the worker
// everything typed in a queued char-by-char way
sync.pyterminal_ready = () => {
let queue = Promise.resolve();
stream = {
write: (buffer) =>
(queue = queue.then(() =>
sync.pyterminal_stream_write(buffer),
)),
};
terminal.onData((buffer) => {
if (promisedChunks) {
// handle backspace on input
if (buffer === "\x7f") {
// avoid over-greedy backspace
if (readChunks.length) {
readChunks = readChunks.slice(0, -1);
// override previous char position
// put an empty space to clear the char
// move back position again
buffer = "\b \b";
} else buffer = "";
} else readChunks += buffer;
if (buffer) {
terminal.write(buffer);
if (readChunks.endsWith("\r")) {
terminal.write("\n");
promisedChunks.resolve(readChunks.slice(0, -1));
promisedChunks = null;
readChunks = "";
}
}
} else {
stream.write(buffer);
}
});
};
});
// setup remote thread JS/Python code for whenever the
// worker is ready to become a terminal
hooks.worker.onReady.add(workerReady);
} else {
// ⚠️ In an ideal world the inputFailure should never be used on main.
// However, Pyodide still can't compete with MicroPython REPL mode
// so while it's OK to keep that entry on main as default, we need
// to remove it ASAP from `mpy` use cases, otherwise MicroPython would
// also throw whenever an `input(...)` is required / digited.
hooks.main.codeBeforeRun.delete(inputFailure);
// in the main case, just bootstrap XTerm without
// allowing any input as that's not possible / awkward
hooks.main.onReady.add(function main({ interpreter, io, run, type }) {
if (type !== "mpy") return;
hooks.main.onReady.delete(main);
const terminal = init();
const missingReturn = new Uint8Array([13]);
io.stdout = (buffer) => {
if (buffer[0] === 10) terminal.write(missingReturn);
terminal.write(buffer);
};
// expose the __terminal__ one-off reference
globalThis.__py_terminal__ = terminal;
run(
[
"from js import prompt as input",
"from js import __py_terminal__ as __terminal__",
].join(";"),
);
delete globalThis.__py_terminal__;
// NOTE: this is NOT the same as the one within
// the onWorkerReady callback!
interpreter.registerJsModule("code", {
interact() {
const encoder = new TextEncoderStream();
encoder.readable.pipeTo(
new WritableStream({
write(buffer) {
for (const c of buffer)
interpreter.replProcessChar(c);
},
}),
);
stream = encoder.writable.getWriter();
terminal.onData((buffer) => stream.write(buffer));
interpreter.replInit();
},
});
});
}
};

View File

@@ -0,0 +1,179 @@
// PyScript py-terminal plugin
import { hooks } from "../../core.js";
import { defineProperties } from "polyscript/exports";
const bootstrapped = new WeakSet();
// this callback will be serialized as string and it never needs
// to be invoked multiple times. Each xworker here is bootstrapped
// only once thanks to the `sync.is_pyterminal()` check.
const workerReady = ({ interpreter, io, run, type }, { sync }) => {
if (type !== "py" || !sync.is_pyterminal()) return;
run(
[
"from polyscript import currentScript as _",
"__terminal__ = _.terminal",
"del _",
].join(";"),
);
let data = "";
const { pyterminal_read, pyterminal_write } = sync;
const decoder = new TextDecoder();
const generic = {
isatty: false,
write(buffer) {
data = decoder.decode(buffer);
pyterminal_write(data);
return buffer.length;
},
};
io.stderr = (error) => {
pyterminal_write(String(error.message || error));
};
interpreter.setStdout(generic);
interpreter.setStderr(generic);
interpreter.setStdin({
isatty: false,
stdin: () => pyterminal_read(data),
});
};
export default async (element) => {
// lazy load these only when a valid terminal is found
const [{ Terminal }, { Readline }, { FitAddon }, { WebLinksAddon }] =
await Promise.all([
import(/* webpackIgnore: true */ "../../3rd-party/xterm.js"),
import(
/* webpackIgnore: true */ "../../3rd-party/xterm-readline.js"
),
import(
/* webpackIgnore: true */ "../../3rd-party/xterm_addon-fit.js"
),
import(
/* webpackIgnore: true */ "../../3rd-party/xterm_addon-web-links.js"
),
]);
const readline = new Readline();
// common main thread initialization for both worker
// or main case, bootstrapping the terminal on its target
const init = (options) => {
let target = element;
const selector = element.getAttribute("target");
if (selector) {
target =
document.getElementById(selector) ||
document.querySelector(selector);
if (!target) throw new Error(`Unknown target ${selector}`);
} else {
target = document.createElement("py-terminal");
target.style.display = "block";
element.after(target);
}
const terminal = new Terminal({
theme: {
background: "#191A19",
foreground: "#F5F2E7",
},
...options,
});
const fitAddon = new FitAddon();
terminal.loadAddon(fitAddon);
terminal.loadAddon(readline);
terminal.loadAddon(new WebLinksAddon());
terminal.open(target);
fitAddon.fit();
terminal.focus();
defineProperties(element, {
terminal: { value: terminal },
process: {
value: async (code) => {
for (const line of code.split(/(?:\r\n|\r|\n)/)) {
terminal.paste(`${line}`);
terminal.write("\r\n");
do {
await new Promise((resolve) =>
setTimeout(resolve, 0),
);
} while (!readline.activeRead?.resolve);
readline.activeRead.resolve(line);
}
},
},
});
return terminal;
};
// branch logic for the worker
if (element.hasAttribute("worker")) {
// add a hook on the main thread to setup all sync helpers
// also bootstrapping the XTerm target on main *BUT* ...
hooks.main.onWorker.add(function worker(_, xworker) {
// ... as multiple workers will add multiple callbacks
// be sure no xworker is ever initialized twice!
if (bootstrapped.has(xworker)) return;
bootstrapped.add(xworker);
// still cleanup this callback for future scripts/workers
hooks.main.onWorker.delete(worker);
init({
disableStdin: false,
cursorBlink: true,
cursorStyle: "block",
});
xworker.sync.is_pyterminal = () => true;
xworker.sync.pyterminal_read = readline.read.bind(readline);
xworker.sync.pyterminal_write = readline.write.bind(readline);
});
// setup remote thread JS/Python code for whenever the
// worker is ready to become a terminal
hooks.worker.onReady.add(workerReady);
} else {
// in the main case, just bootstrap XTerm without
// allowing any input as that's not possible / awkward
hooks.main.onReady.add(function main({ interpreter, io, run, type }) {
if (type !== "py") return;
console.warn("py-terminal is read only on main thread");
hooks.main.onReady.delete(main);
// on main, it's easy to trash and clean the current terminal
globalThis.__py_terminal__ = init({
disableStdin: true,
cursorBlink: false,
cursorStyle: "underline",
});
run("from js import __py_terminal__ as __terminal__");
delete globalThis.__py_terminal__;
io.stderr = (error) => {
readline.write(String(error.message || error));
};
let data = "";
const decoder = new TextDecoder();
const generic = {
isatty: false,
write(buffer) {
data = decoder.decode(buffer);
readline.write(data);
return buffer.length;
},
};
interpreter.setStdout(generic);
interpreter.setStderr(generic);
interpreter.setStdin({
isatty: false,
stdin: () => readline.read(data),
});
});
}
};

View File

@@ -8,6 +8,27 @@
import pyscript from "./stdlib/pyscript.js";
class Ignore extends Array {
#add = false;
#paths;
#array;
constructor(array, ...paths) {
super();
this.#array = array;
this.#paths = paths;
}
push(...values) {
if (this.#add) super.push(...values);
return this.#array.push(...values);
}
path(path) {
for (const _path of this.#paths) {
// bails out at the first `true` value
if ((this.#add = path.startsWith(_path))) break;
}
}
}
const { entries } = Object;
const python = [
@@ -16,16 +37,19 @@ const python = [
"_path = None",
];
const ignore = new Ignore(python, "-");
const write = (base, literal) => {
for (const [key, value] of entries(literal)) {
python.push(`_path = _Path("${base}/${key}")`);
ignore.path(`${base}/${key}`);
ignore.push(`_path = _Path("${base}/${key}")`);
if (typeof value === "string") {
const code = JSON.stringify(value);
python.push(`_path.write_text(${code},encoding="utf-8")`);
ignore.push(`_path.write_text(${code},encoding="utf-8")`);
} else {
// @see https://github.com/pyscript/pyscript/pull/1813#issuecomment-1781502909
python.push(`if not _os.path.exists("${base}/${key}"):`);
python.push(" _path.mkdir(parents=True, exist_ok=True)");
ignore.push(`if not _os.path.exists("${base}/${key}"):`);
ignore.push(" _path.mkdir(parents=True, exist_ok=True)");
write(`${base}/${key}`, value);
}
}
@@ -42,4 +66,5 @@ python.push(
);
python.push("\n");
export default python.join("\n");
export const stdlib = python.join("\n");
export const optional = ignore.join("\n");

View File

@@ -29,17 +29,25 @@
# pyscript.magic_js. This is the blessed way to access them from pyscript,
# as it works transparently in both the main thread and worker cases.
from polyscript import lazy_py_modules as py_import
from pyscript.display import HTML, display
from pyscript.fetch import fetch
from pyscript.magic_js import (
RUNNING_IN_WORKER,
PyWorker,
config,
current_target,
document,
js_import,
js_modules,
sync,
window,
)
from pyscript.storage import Storage, storage
from pyscript.websocket import WebSocket
if not RUNNING_IN_WORKER:
from pyscript.workers import create_named_worker, workers
try:
from pyscript.event_handling import when

View File

@@ -6,17 +6,17 @@ import re
from pyscript.magic_js import current_target, document, window
_MIME_METHODS = {
"__repr__": "text/plain",
"_repr_html_": "text/html",
"_repr_markdown_": "text/markdown",
"_repr_svg_": "image/svg+xml",
"_repr_pdf_": "application/pdf",
"_repr_jpeg_": "image/jpeg",
"_repr_png_": "image/png",
"_repr_latex": "text/latex",
"_repr_json_": "application/json",
"_repr_javascript_": "application/javascript",
"savefig": "image/png",
"_repr_javascript_": "application/javascript",
"_repr_json_": "application/json",
"_repr_latex": "text/latex",
"_repr_png_": "image/png",
"_repr_jpeg_": "image/jpeg",
"_repr_pdf_": "application/pdf",
"_repr_svg_": "image/svg+xml",
"_repr_markdown_": "text/markdown",
"_repr_html_": "text/html",
"__repr__": "text/plain",
}
@@ -99,7 +99,7 @@ def _format_mime(obj):
format_dict = mimebundle
output, not_available = None, []
for method, mime_type in reversed(_MIME_METHODS.items()):
for method, mime_type in _MIME_METHODS.items():
if mime_type in format_dict:
output = format_dict[mime_type]
else:

View File

@@ -19,22 +19,23 @@ def when(event_type=None, selector=None):
"""
def decorator(func):
from pyscript.web import Element, ElementCollection
if isinstance(selector, str):
elements = document.querySelectorAll(selector)
# TODO: This is a hack that will be removed when pyscript becomes a package
# and we can better manage the imports without circular dependencies
elif isinstance(selector, Element):
elements = [selector._dom_element]
elif isinstance(selector, ElementCollection):
elements = [el._dom_element for el in selector]
else:
# TODO: This is a hack that will be removed when pyscript becomes a package
# and we can better manage the imports without circular dependencies
from pyweb import pydom
if isinstance(selector, pydom.Element):
elements = [selector._js]
elif isinstance(selector, pydom.ElementCollection):
elements = [el._js for el in selector]
if isinstance(selector, list):
elements = selector
else:
raise ValueError(
f"Invalid selector: {selector}. Selector must"
" be a string, a pydom.Element or a pydom.ElementCollection."
)
elements = [selector]
try:
sig = inspect.signature(func)
# Function doesn't receive events
@@ -47,13 +48,14 @@ def when(event_type=None, selector=None):
wrapper = func
except AttributeError:
# TODO: this is currently an quick hack to get micropython working but we need
# to actually properly replace inspect.signature with something else
# TODO: this is very ugly hack to get micropython working because inspect.signature
# doesn't exist, but we need to actually properly replace inspect.signature.
# It may be actually better to not try any magic for now and raise the error
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except TypeError as e:
if "takes 0 positional arguments" in str(e):
if "takes" in str(e) and "positional arguments" in str(e):
return func()
raise

View File

@@ -1,64 +1,87 @@
import json
import js
from pyscript.util import as_bytearray
def _as_bytearray(buffer):
ui8a = js.Uint8Array.new(buffer)
size = ui8a.length
ba = bytearray(size)
for i in range(0, size):
ba[i] = ui8a[i]
return ba
### wrap the response to grant Pythonic results
class _Response:
def __init__(self, response):
self._response = response
# grant access to response.ok and other fields
def __getattr__(self, attr):
return getattr(self._response, attr)
class _Fetch:
def __init__(self, url, **kw):
# avoid both Pyodide and MicroPython FFI
options = js.JSON.parse(json.dumps(kw))
self._url = url
self._fetch = js.fetch(url, options)
async def _arrayBuffer(self):
response = await self._response()
return await response.arrayBuffer()
async def _response(self):
response = await self._fetch
if not response.ok:
msg = f"URL {self._url} failed with status {response.status}"
raise Exception(msg)
return response
# https://developer.mozilla.org/en-US/docs/Web/API/Response/arrayBuffer
# returns a memoryview of the buffer
# exposed methods with Pythonic results
async def arrayBuffer(self):
buffer = await self._arrayBuffer()
buffer = await self._response.arrayBuffer()
# works in Pyodide
if hasattr(buffer, "to_py"):
return buffer.to_py()
# shims in MicroPython
return memoryview(_as_bytearray(buffer))
return memoryview(as_bytearray(buffer))
async def blob(self):
return await self._response.blob()
async def bytearray(self):
buffer = await self._response.arrayBuffer()
return as_bytearray(buffer)
async def json(self):
return json.loads(await self.text())
async def text(self):
return await self._response.text()
### allow direct await to _Response methods
class _DirectResponse:
@staticmethod
def setup(promise, response):
promise._response = _Response(response)
return promise._response
def __init__(self, promise):
self._promise = promise
promise._response = None
promise.arrayBuffer = self.arrayBuffer
promise.blob = self.blob
promise.bytearray = self.bytearray
promise.json = self.json
promise.text = self.text
async def _response(self):
if not self._promise._response:
await self._promise
return self._promise._response
async def arrayBuffer(self):
response = await self._response()
return await response.arrayBuffer()
# https://developer.mozilla.org/en-US/docs/Web/API/Response/blob
async def blob(self):
response = await self._response()
return await response.blob()
# return a bytearray from the uint8 view of the buffer
async def bytearray(self):
buffer = await self._arrayBuffer()
return _as_bytearray(buffer)
response = await self._response()
return await response.bytearray()
# https://developer.mozilla.org/en-US/docs/Web/API/Response/json
async def json(self):
return json.loads(await self.text())
response = await self._response()
return await response.json()
# https://developer.mozilla.org/en-US/docs/Web/API/Response/text
async def text(self):
response = await self._response()
return await response.text()
def fetch(url, **kw):
return _Fetch(url, **kw)
# workaround Pyodide / MicroPython dict <-> js conversion
options = js.JSON.parse(json.dumps(kw))
awaited = lambda response, *args: _DirectResponse.setup(promise, response)
promise = js.fetch(url, options).then(awaited)
_DirectResponse(promise)
return promise

View File

@@ -0,0 +1,18 @@
try:
import js
from pyodide.ffi import create_proxy as _cp
from pyodide.ffi import to_js as _py_tjs
from_entries = js.Object.fromEntries
def _tjs(value, **kw):
if not hasattr(kw, "dict_converter"):
kw["dict_converter"] = from_entries
return _py_tjs(value, **kw)
except:
from jsffi import create_proxy as _cp
from jsffi import to_js as _tjs
create_proxy = _cp
to_js = _tjs

View File

@@ -0,0 +1,148 @@
# https://www.npmjs.com/package/flatted
import json as _json
class _Known:
def __init__(self):
self.key = []
self.value = []
class _String:
def __init__(self, value):
self.value = value
def _array_keys(value):
keys = []
i = 0
for _ in value:
keys.append(i)
i += 1
return keys
def _object_keys(value):
keys = []
for key in value:
keys.append(key)
return keys
def _is_array(value):
return isinstance(value, list) or isinstance(value, tuple)
def _is_object(value):
return isinstance(value, dict)
def _is_string(value):
return isinstance(value, str)
def _index(known, input, value):
input.append(value)
index = str(len(input) - 1)
known.key.append(value)
known.value.append(index)
return index
def _loop(keys, input, known, output):
for key in keys:
value = output[key]
if isinstance(value, _String):
_ref(key, input[int(value.value)], input, known, output)
return output
def _ref(key, value, input, known, output):
if _is_array(value) and not value in known:
known.append(value)
value = _loop(_array_keys(value), input, known, value)
elif _is_object(value) and not value in known:
known.append(value)
value = _loop(_object_keys(value), input, known, value)
output[key] = value
def _relate(known, input, value):
if _is_string(value) or _is_array(value) or _is_object(value):
try:
return known.value[known.key.index(value)]
except:
return _index(known, input, value)
return value
def _transform(known, input, value):
if _is_array(value):
output = []
for val in value:
output.append(_relate(known, input, val))
return output
if _is_object(value):
obj = {}
for key in value:
obj[key] = _relate(known, input, value[key])
return obj
return value
def _wrap(value):
if _is_string(value):
return _String(value)
if _is_array(value):
i = 0
for val in value:
value[i] = _wrap(val)
i += 1
elif _is_object(value):
for key in value:
value[key] = _wrap(value[key])
return value
def parse(value, *args, **kwargs):
json = _json.loads(value, *args, **kwargs)
wrapped = []
for value in json:
wrapped.append(_wrap(value))
input = []
for value in wrapped:
if isinstance(value, _String):
input.append(value.value)
else:
input.append(value)
value = input[0]
if _is_array(value):
return _loop(_array_keys(value), input, [value], value)
if _is_object(value):
return _loop(_object_keys(value), input, [value], value)
return value
def stringify(value, *args, **kwargs):
known = _Known()
input = []
output = []
i = int(_index(known, input, value))
while i < len(input):
output.append(_transform(known, input, input[i]))
i += 1
return _json.dumps(output, *args, **kwargs)

View File

@@ -1,11 +1,15 @@
import json
import sys
import js as globalThis
from polyscript import config as _config
from polyscript import js_modules
from pyscript.util import NotSupported
RUNNING_IN_WORKER = not hasattr(globalThis, "document")
config = json.loads(globalThis.JSON.stringify(_config))
# allow `from pyscript.js_modules.xxx import yyy`
class JSModule:
@@ -32,24 +36,21 @@ if RUNNING_IN_WORKER:
)
try:
globalThis.SharedArrayBuffer.new(4)
import js
window = polyscript.xworker.window
document = window.document
js.document = document
# this is the same as js_import on main and it lands modules on main
js_import = window.Function(
"return (...urls) => Promise.all(urls.map((url) => import(url)))"
)()
except:
globalThis.console.debug("SharedArrayBuffer is not available")
# in this scenario none of the utilities would work
# as expected so we better export these as NotSupported
window = NotSupported(
"pyscript.window",
"pyscript.window in workers works only via SharedArrayBuffer",
)
document = NotSupported(
"pyscript.document",
"pyscript.document in workers works only via SharedArrayBuffer",
)
message = "Unable to use `window` or `document` -> https://docs.pyscript.net/latest/faq/#sharedarraybuffer"
globalThis.console.warn(message)
window = NotSupported("pyscript.window", message)
document = NotSupported("pyscript.document", message)
js_import = None
sync = polyscript.xworker.sync
@@ -60,7 +61,7 @@ if RUNNING_IN_WORKER:
else:
import _pyscript
from _pyscript import PyWorker
from _pyscript import PyWorker, js_import
window = globalThis
document = globalThis.document

View File

@@ -0,0 +1,60 @@
from polyscript import storage as _storage
from pyscript.flatted import parse as _parse
from pyscript.flatted import stringify as _stringify
# convert a Python value into an IndexedDB compatible entry
def _to_idb(value):
if value is None:
return _stringify(["null", 0])
if isinstance(value, (bool, float, int, str, list, dict, tuple)):
return _stringify(["generic", value])
if isinstance(value, bytearray):
return _stringify(["bytearray", [v for v in value]])
if isinstance(value, memoryview):
return _stringify(["memoryview", [v for v in value]])
raise TypeError(f"Unexpected value: {value}")
# convert an IndexedDB compatible entry into a Python value
def _from_idb(value):
(
kind,
result,
) = _parse(value)
if kind == "null":
return None
if kind == "generic":
return result
if kind == "bytearray":
return bytearray(result)
if kind == "memoryview":
return memoryview(bytearray(result))
return value
class Storage(dict):
def __init__(self, store):
super().__init__({k: _from_idb(v) for k, v in store.entries()})
self.__store__ = store
def __delitem__(self, attr):
self.__store__.delete(attr)
super().__delitem__(attr)
def __setitem__(self, attr, value):
self.__store__.set(attr, _to_idb(value))
super().__setitem__(attr, value)
def clear(self):
self.__store__.clear()
super().clear()
async def sync(self):
await self.__store__.sync()
async def storage(name="", storage_class=Storage):
if not name:
raise ValueError("The storage name must be defined")
return storage_class(await _storage(f"@pyscript/{name}"))

View File

@@ -1,3 +1,15 @@
import js
def as_bytearray(buffer):
ui8a = js.Uint8Array.new(buffer)
size = ui8a.length
ba = bytearray(size)
for i in range(0, size):
ba[i] = ui8a[i]
return ba
class NotSupported:
"""
Small helper that raises exceptions if you try to get/set any attribute on

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,67 @@
import js
from pyscript.util import as_bytearray
code = "code"
protocols = "protocols"
reason = "reason"
class EventMessage:
def __init__(self, event):
self._event = event
def __getattr__(self, attr):
value = getattr(self._event, attr)
if attr == "data" and not isinstance(value, str):
if hasattr(value, "to_py"):
return value.to_py()
# shims in MicroPython
return memoryview(as_bytearray(value))
return value
class WebSocket(object):
CONNECTING = 0
OPEN = 1
CLOSING = 2
CLOSED = 3
def __init__(self, **kw):
url = kw["url"]
if protocols in kw:
socket = js.WebSocket.new(url, kw[protocols])
else:
socket = js.WebSocket.new(url)
object.__setattr__(self, "_ws", socket)
for t in ["onclose", "onerror", "onmessage", "onopen"]:
if t in kw:
socket[t] = kw[t]
def __getattr__(self, attr):
return getattr(self._ws, attr)
def __setattr__(self, attr, value):
if attr == "onmessage":
self._ws[attr] = lambda e: value(EventMessage(e))
else:
self._ws[attr] = value
def close(self, **kw):
if code in kw and reason in kw:
self._ws.close(kw[code], kw[reason])
elif code in kw:
self._ws.close(kw[code])
else:
self._ws.close()
def send(self, data):
if isinstance(data, str):
self._ws.send(data)
else:
buffer = js.Uint8Array.new(len(data))
for pos, b in enumerate(data):
buffer[pos] = b
self._ws.send(buffer)

View File

@@ -0,0 +1,43 @@
import js as _js
from polyscript import workers as _workers
_get = _js.Reflect.get
def _set(script, name, value=""):
script.setAttribute(name, value)
# this solves an inconsistency between Pyodide and MicroPython
# @see https://github.com/pyscript/pyscript/issues/2106
class _ReadOnlyProxy:
def __getitem__(self, name):
return _get(_workers, name)
def __getattr__(self, name):
return _get(_workers, name)
workers = _ReadOnlyProxy()
async def create_named_worker(src="", name="", config=None, type="py"):
from json import dumps
if not src:
raise ValueError("Named workers require src")
if not name:
raise ValueError("Named workers require a name")
s = _js.document.createElement("script")
s.type = type
s.src = src
_set(s, "worker")
_set(s, "name", name)
if config:
_set(s, "config", isinstance(config, str) and config or dumps(config))
_js.document.body.append(s)
return await workers[name]

View File

@@ -1 +0,0 @@
from .pydom import dom as pydom

View File

@@ -1,95 +0,0 @@
from pyodide.ffi import to_js
from pyscript import window
class Device:
"""Device represents a media input or output device, such as a microphone,
camera, or headset.
"""
def __init__(self, device):
self._js = device
@property
def id(self):
return self._js.deviceId
@property
def group(self):
return self._js.groupId
@property
def kind(self):
return self._js.kind
@property
def label(self):
return self._js.label
def __getitem__(self, key):
return getattr(self, key)
@classmethod
async def load(cls, audio=False, video=True):
"""Load the device stream."""
options = window.Object.new()
options.audio = audio
if isinstance(video, bool):
options.video = video
else:
# TODO: Think this can be simplified but need to check it on the pyodide side
# TODO: this is pyodide specific. shouldn't be!
options.video = window.Object.new()
for k in video:
setattr(
options.video,
k,
to_js(video[k], dict_converter=window.Object.fromEntries),
)
stream = await window.navigator.mediaDevices.getUserMedia(options)
return stream
async def get_stream(self):
key = self.kind.replace("input", "").replace("output", "")
options = {key: {"deviceId": {"exact": self.id}}}
return await self.load(**options)
async def list_devices() -> list[dict]:
"""
Return the list of the currently available media input and output devices,
such as microphones, cameras, headsets, and so forth.
Output:
list(dict) - list of dictionaries representing the available media devices.
Each dictionary has the following keys:
* deviceId: a string that is an identifier for the represented device
that is persisted across sessions. It is un-guessable by other
applications and unique to the origin of the calling application.
It is reset when the user clears cookies (for Private Browsing, a
different identifier is used that is not persisted across sessions).
* groupId: a string that is a group identifier. Two devices have the same
group identifier if they belong to the same physical device — for
example a monitor with both a built-in camera and a microphone.
* kind: an enumerated value that is either "videoinput", "audioinput"
or "audiooutput".
* label: a string describing this device (for example "External USB
Webcam").
Note: the returned list will omit any devices that are blocked by the document
Permission Policy: microphone, camera, speaker-selection (for output devices),
and so on. Access to particular non-default devices is also gated by the
Permissions API, and the list will omit devices for which the user has not
granted explicit permission.
"""
# https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/enumerateDevices
return [
Device(obj) for obj in await window.navigator.mediaDevices.enumerateDevices()
]

View File

@@ -1,550 +0,0 @@
try:
from typing import Any
except ImportError:
Any = "Any"
try:
import warnings
except ImportError:
# TODO: For now it probably means we are in MicroPython. We should figure
# out the "right" way to handle this. For now we just ignore the warning
# and logging to console
class warnings:
@staticmethod
def warn(*args, **kwargs):
print("WARNING: ", *args, **kwargs)
try:
from functools import cached_property
except ImportError:
# TODO: same comment about micropython as above
cached_property = property
try:
from pyodide.ffi import JsProxy
except ImportError:
# TODO: same comment about micropython as above
def JsProxy(obj):
return obj
from pyscript import display, document, window
alert = window.alert
class BaseElement:
def __init__(self, js_element):
self._js = js_element
self._parent = None
self.style = StyleProxy(self)
self._proxies = {}
def __eq__(self, obj):
"""Check if the element is the same as the other element by comparing
the underlying JS element"""
return isinstance(obj, BaseElement) and obj._js == self._js
@property
def parent(self):
if self._parent:
return self._parent
if self._js.parentElement:
self._parent = self.__class__(self._js.parentElement)
return self._parent
@property
def __class(self):
return self.__class__ if self.__class__ != PyDom else Element
def create(self, type_, is_child=True, classes=None, html=None, label=None):
js_el = document.createElement(type_)
element = self.__class(js_el)
if classes:
for class_ in classes:
element.add_class(class_)
if html is not None:
element.html = html
if label is not None:
element.label = label
if is_child:
self.append(element)
return element
def find(self, selector):
"""Return an ElementCollection representing all the child elements that
match the specified selector.
Args:
selector (str): A string containing a selector expression
Returns:
ElementCollection: A collection of elements matching the selector
"""
elements = self._js.querySelectorAll(selector)
if not elements:
return None
return ElementCollection([Element(el) for el in elements])
class Element(BaseElement):
@property
def children(self):
return [self.__class__(el) for el in self._js.children]
def append(self, child):
# TODO: this is Pyodide specific for now!!!!!!
# if we get passed a JSProxy Element directly we just map it to the
# higher level Python element
if isinstance(child, JsProxy):
return self.append(Element(child))
elif isinstance(child, Element):
self._js.appendChild(child._js)
return child
elif isinstance(child, ElementCollection):
for el in child:
self.append(el)
# -------- Pythonic Interface to Element -------- #
@property
def html(self):
return self._js.innerHTML
@html.setter
def html(self, value):
self._js.innerHTML = value
@property
def text(self):
return self._js.textContent
@text.setter
def text(self, value):
self._js.textContent = value
@property
def content(self):
# TODO: This breaks with with standard template elements. Define how to best
# handle this specifica use case. Just not support for now?
if self._js.tagName == "TEMPLATE":
warnings.warn(
"Content attribute not supported for template elements.", stacklevel=2
)
return None
return self._js.innerHTML
@content.setter
def content(self, value):
# TODO: (same comment as above)
if self._js.tagName == "TEMPLATE":
warnings.warn(
"Content attribute not supported for template elements.", stacklevel=2
)
return
display(value, target=self.id)
@property
def id(self):
return self._js.id
@id.setter
def id(self, value):
self._js.id = value
@property
def options(self):
if "options" in self._proxies:
return self._proxies["options"]
if not self._js.tagName.lower() in {"select", "datalist", "optgroup"}:
raise AttributeError(
f"Element {self._js.tagName} has no options attribute."
)
self._proxies["options"] = OptionsProxy(self)
return self._proxies["options"]
@property
def value(self):
return self._js.value
@value.setter
def value(self, value):
# in order to avoid confusion to the user, we don't allow setting the
# value of elements that don't have a value attribute
if not hasattr(self._js, "value"):
raise AttributeError(
f"Element {self._js.tagName} has no value attribute. If you want to "
"force a value attribute, set it directly using the `_js.value = <value>` "
"javascript API attribute instead."
)
self._js.value = value
@property
def selected(self):
return self._js.selected
@selected.setter
def selected(self, value):
# in order to avoid confusion to the user, we don't allow setting the
# value of elements that don't have a value attribute
if not hasattr(self._js, "selected"):
raise AttributeError(
f"Element {self._js.tagName} has no value attribute. If you want to "
"force a value attribute, set it directly using the `_js.value = <value>` "
"javascript API attribute instead."
)
self._js.selected = value
def clone(self, new_id=None):
clone = Element(self._js.cloneNode(True))
clone.id = new_id
return clone
def remove_class(self, classname):
classList = self._js.classList
if isinstance(classname, list):
classList.remove(*classname)
else:
classList.remove(classname)
return self
def add_class(self, classname):
classList = self._js.classList
if isinstance(classname, list):
classList.add(*classname)
else:
self._js.classList.add(classname)
return self
@property
def classes(self):
classes = self._js.classList.values()
return [x for x in classes]
def show_me(self):
self._js.scrollIntoView()
def snap(
self,
to: BaseElement | str = None,
width: int | None = None,
height: int | None = None,
):
"""
Captures a snapshot of a video element. (Only available for video elements)
Inputs:
* to: element where to save the snapshot of the video frame to
* width: width of the image
* height: height of the image
Output:
(Element) canvas element where the video frame snapshot was drawn into
"""
if self._js.tagName != "VIDEO":
raise AttributeError("Snap method is only available for video Elements")
if to is None:
canvas = self.create("canvas")
if width is None:
width = self._js.width
if height is None:
height = self._js.height
canvas._js.width = width
canvas._js.height = height
elif isistance(to, Element):
if to._js.tagName != "CANVAS":
raise TypeError("Element to snap to must a canvas.")
canvas = to
elif getattr(to, "tagName", "") == "CANVAS":
canvas = Element(to)
elif isinstance(to, str):
canvas = pydom[to][0]
if canvas._js.tagName != "CANVAS":
raise TypeError("Element to snap to must a be canvas.")
canvas.draw(self, width, height)
return canvas
def download(self, filename: str = "snapped.png") -> None:
"""Download the current element (only available for canvas elements) with the filename
provided in input.
Inputs:
* filename (str): name of the file being downloaded
Output:
None
"""
if self._js.tagName != "CANVAS":
raise AttributeError(
"The download method is only available for canvas Elements"
)
link = self.create("a")
link._js.download = filename
link._js.href = self._js.toDataURL()
link._js.click()
def draw(self, what, width, height):
"""Draw `what` on the current element (only available for canvas elements).
Inputs:
* what (canvas image source): An element to draw into the context. The specification permits any canvas
image source, specifically, an HTMLImageElement, an SVGImageElement, an HTMLVideoElement,
an HTMLCanvasElement, an ImageBitmap, an OffscreenCanvas, or a VideoFrame.
"""
if self._js.tagName != "CANVAS":
raise AttributeError(
"The draw method is only available for canvas Elements"
)
if isinstance(what, Element):
what = what._js
# https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage
self._js.getContext("2d").drawImage(what, 0, 0, width, height)
class OptionsProxy:
"""This class represents the options of a select element. It
allows to access to add and remove options by using the `add` and `remove` methods.
"""
def __init__(self, element: Element) -> None:
self._element = element
if self._element._js.tagName.lower() != "select":
raise AttributeError(
f"Element {self._element._js.tagName} has no options attribute."
)
def add(
self,
value: Any = None,
html: str = None,
text: str = None,
before: Element | int = None,
**kws,
) -> None:
"""Add a new option to the select element"""
# create the option element and set the attributes
option = document.createElement("option")
if value is not None:
kws["value"] = value
if html is not None:
option.innerHTML = html
if text is not None:
kws["text"] = text
for key, value in kws.items():
option.setAttribute(key, value)
if before:
if isinstance(before, Element):
before = before._js
self._element._js.add(option, before)
def remove(self, item: int) -> None:
"""Remove the option at the specified index"""
self._element._js.remove(item)
def clear(self) -> None:
"""Remove all the options"""
for i in range(len(self)):
self.remove(0)
@property
def options(self):
"""Return the list of options"""
return [Element(opt) for opt in self._element._js.options]
@property
def selected(self):
"""Return the selected option"""
return self.options[self._element._js.selectedIndex]
def __iter__(self):
yield from self.options
def __len__(self):
return len(self.options)
def __repr__(self):
return f"{self.__class__.__name__} (length: {len(self)}) {self.options}"
def __getitem__(self, key):
return self.options[key]
class StyleProxy: # (dict):
def __init__(self, element: Element) -> None:
self._element = element
@cached_property
def _style(self):
return self._element._js.style
def __getitem__(self, key):
return self._style.getPropertyValue(key)
def __setitem__(self, key, value):
self._style.setProperty(key, value)
def remove(self, key):
self._style.removeProperty(key)
def set(self, **kws):
for k, v in kws.items():
self._element._js.style.setProperty(k, v)
# CSS Properties
# Reference: https://github.com/microsoft/TypeScript/blob/main/src/lib/dom.generated.d.ts#L3799C1-L5005C2
# Following prperties automatically generated from the above reference using
# tools/codegen_css_proxy.py
@property
def visible(self):
return self._element._js.style.visibility
@visible.setter
def visible(self, value):
self._element._js.style.visibility = value
class StyleCollection:
def __init__(self, collection: "ElementCollection") -> None:
self._collection = collection
def __get__(self, obj, objtype=None):
return obj._get_attribute("style")
def __getitem__(self, key):
return self._collection._get_attribute("style")[key]
def __setitem__(self, key, value):
for element in self._collection._elements:
element.style[key] = value
def remove(self, key):
for element in self._collection._elements:
element.style.remove(key)
class ElementCollection:
def __init__(self, elements: [Element]) -> None:
self._elements = elements
self.style = StyleCollection(self)
def __getitem__(self, key):
# If it's an integer we use it to access the elements in the collection
if isinstance(key, int):
return self._elements[key]
# If it's a slice we use it to support slice operations over the elements
# in the collection
elif isinstance(key, slice):
return ElementCollection(self._elements[key])
# If it's anything else (basically a string) we use it as a selector
# TODO: Write tests!
elements = self._element.querySelectorAll(key)
return ElementCollection([Element(el) for el in elements])
def __len__(self):
return len(self._elements)
def __eq__(self, obj):
"""Check if the element is the same as the other element by comparing
the underlying JS element"""
return isinstance(obj, ElementCollection) and obj._elements == self._elements
def _get_attribute(self, attr, index=None):
if index is None:
return [getattr(el, attr) for el in self._elements]
# As JQuery, when getting an attr, only return it for the first element
return getattr(self._elements[index], attr)
def _set_attribute(self, attr, value):
for el in self._elements:
setattr(el, attr, value)
@property
def html(self):
return self._get_attribute("html")
@html.setter
def html(self, value):
self._set_attribute("html", value)
@property
def value(self):
return self._get_attribute("value")
@value.setter
def value(self, value):
self._set_attribute("value", value)
@property
def children(self):
return self._elements
def __iter__(self):
yield from self._elements
def __repr__(self):
return f"{self.__class__.__name__} (length: {len(self._elements)}) {self._elements}"
class DomScope:
def __getattr__(self, __name: str):
element = document[f"#{__name}"]
if element:
return element[0]
class PyDom(BaseElement):
# Add objects we want to expose to the DOM namespace since this class instance is being
# remapped as "the module" itself
BaseElement = BaseElement
Element = Element
ElementCollection = ElementCollection
def __init__(self):
# PyDom is a special case of BaseElement where we don't want to create a new JS element
# and it really doesn't have a need for styleproxy or parent to to call to __init__
# (which actually fails in MP for some reason)
self._js = document
self._parent = None
self._proxies = {}
self.ids = DomScope()
self.body = Element(document.body)
self.head = Element(document.head)
def create(self, type_, classes=None, html=None):
return super().create(type_, is_child=False, classes=classes, html=html)
def __getitem__(self, key):
elements = self._js.querySelectorAll(key)
if not elements:
return None
return ElementCollection([Element(el) for el in elements])
dom = PyDom()

View File

@@ -5,11 +5,17 @@
<script type="module" src="../dist/core.js"></script>
</head>
<body>
<py-script async>
<py-script>
import asyncio
print('foo')
print('py-script sleep')
await asyncio.sleep(1)
print('bar')
print('py-script done')
</py-script>
<script type="py">
import asyncio
print('script-py sleep')
await asyncio.sleep(1)
print('script-py done')
</script>
</body>
</html>

View File

@@ -8,9 +8,17 @@
<script type="module" src="../dist/core.js"></script>
<mpy-config src="config-url/config.json"></mpy-config>
<script type="mpy">
from pyscript import config
if config["files"]["{TO}"] != "./runtime":
raise Exception("wrong config tree")
from runtime import test
</script>
<script type="mpy" worker>
from pyscript import config
if config["files"]["{TO}"] != "./runtime":
raise Exception("wrong config tree")
from runtime import test
</script>
</head>

View File

@@ -24,51 +24,71 @@
);
</script>
<script type="mpy" async>
import js, json
from pyscript import document, fetch
fetch_text = await fetch("config.json").text()
if (fetch_text != js.fetch_text):
raise Exception("fetch_text")
fetch_json = await fetch("config.json").json()
if (json.dumps(fetch_json).replace(" ", "") != js.fetch_json):
raise Exception("fetch_json")
fetch_buffer = await fetch("config.json").bytearray()
if (len(fetch_buffer) != js.fetch_buffer):
raise Exception("fetch_buffer")
print(await fetch("config.json").bytearray())
print(await fetch("config.json").blob())
try:
await fetch("shenanigans.nope").text()
except:
document.documentElement.classList.add('mpy')
</script>
<script type="py" async>
import js, json
from pyscript import document, fetch
fetch_text = await (await fetch("config.json")).text()
if (fetch_text != js.fetch_text):
raise Exception("fetch_text")
fetch_text = await fetch("config.json").text()
if (fetch_text != js.fetch_text):
raise Exception("fetch_text")
fetch_json = await (await fetch("config.json")).json()
if (json.dumps(fetch_json).replace(" ", "") != js.fetch_json):
raise Exception("fetch_json")
fetch_json = await fetch("config.json").json()
if (json.dumps(fetch_json).replace(" ", "") != js.fetch_json):
raise Exception("fetch_json")
fetch_buffer = await fetch("config.json").bytearray()
fetch_buffer = await (await fetch("config.json")).arrayBuffer()
if (len(fetch_buffer) != js.fetch_buffer):
raise Exception("fetch_buffer")
print(await fetch("config.json").bytearray())
print(await fetch("config.json").blob())
fetch_buffer = await fetch("config.json").arrayBuffer()
if (len(fetch_buffer) != js.fetch_buffer):
raise Exception("fetch_buffer")
try:
await fetch("shenanigans.nope").text()
except:
print(await (await fetch("config.json")).bytearray())
print(await (await fetch("config.json")).blob())
if (await fetch("shenanigans.nope")).ok == False:
document.documentElement.classList.add('mpy')
</script>
<script type="py" async>
import js, json
from pyscript import document, fetch
fetch_text = await (await fetch("config.json")).text()
if (fetch_text != js.fetch_text):
raise Exception("fetch_text")
fetch_text = await fetch("config.json").text()
if (fetch_text != js.fetch_text):
raise Exception("fetch_text")
fetch_json = await (await fetch("config.json")).json()
if (json.dumps(fetch_json).replace(" ", "") != js.fetch_json):
raise Exception("fetch_json")
fetch_json = await fetch("config.json").json()
if (json.dumps(fetch_json).replace(" ", "") != js.fetch_json):
raise Exception("fetch_json")
fetch_buffer = await (await fetch("config.json")).arrayBuffer()
if (len(fetch_buffer) != js.fetch_buffer):
raise Exception("fetch_buffer")
fetch_buffer = await fetch("config.json").arrayBuffer()
if (len(fetch_buffer) != js.fetch_buffer):
raise Exception("fetch_buffer")
print(await (await fetch("config.json")).bytearray())
print(await (await fetch("config.json")).blob())
if (await fetch("shenanigans.nope")).ok == False:
document.documentElement.classList.add('py')
</script>
</body>

View File

@@ -0,0 +1,26 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PyScript FFI</title>
<link rel="stylesheet" href="../dist/core.css">
<script type="module" src="../dist/core.js"></script>
</head>
<body>
<script type="mpy">
from pyscript import document
from pyscript.ffi import to_js
document.documentElement.classList.add(
to_js({"ok": "mpy"}).ok
)
</script>
<script type="py">
from pyscript import document
from pyscript.ffi import to_js
document.documentElement.classList.add(
to_js({"ok": "py"}).ok
)
</script>
</body>
</html>

View File

@@ -48,12 +48,12 @@
</script>
</head>
<body>
<script type="mpy" worker>
<script type="mpy" async="false" worker>
from pyscript import document
print("actual code in worker")
document.documentElement.classList.add('worker')
</script>
<script type="mpy">
<script type="mpy" async="false">
print("actual code in main")
</script>
</body>

View File

@@ -0,0 +1,4 @@
packages = [
"https://cdn.holoviz.org/panel/wheels/bokeh-3.5.0-py3-none-any.whl",
"https://cdn.holoviz.org/panel/1.5.0-b.2/dist/wheels/panel-1.5.0b2-py3-none-any.whl"
]

View File

@@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="../../dist/core.css">
<script src="https://cdn.bokeh.org/bokeh/release/bokeh-3.5.0.js"></script>
<script src="https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.5.0.min.js"></script>
<script src="https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.5.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@holoviz/panel@1.5.0-b.2/dist/panel.min.js"></script>
<script type="module" src="../../dist/core.js"></script>
</head>
<body>
<script type="py" src="main.py" config="config.toml" worker></script>
<div id="simple_app"></div>
</body>
</html>

View File

@@ -0,0 +1,12 @@
import panel as pn
pn.extension(sizing_mode="stretch_width")
slider = pn.widgets.FloatSlider(start=0, end=10, name="amplitude")
def callback(new):
return f"Amplitude is: {new}"
pn.Row(slider, pn.bind(callback, slider)).servable(target="simple_app")

View File

@@ -83,3 +83,18 @@ test('MicroPython + Pyodide fetch', async ({ page }) => {
await page.goto('http://localhost:8080/test/fetch.html');
await page.waitForSelector('html.mpy.py');
});
test('MicroPython + Pyodide ffi', async ({ page }) => {
await page.goto('http://localhost:8080/test/ffi.html');
await page.waitForSelector('html.mpy.py');
});
test('MicroPython + Storage', async ({ page }) => {
await page.goto('http://localhost:8080/test/storage.html');
await page.waitForSelector('html.ok');
});
test('MicroPython + workers', async ({ page }) => {
await page.goto('http://localhost:8080/test/workers/index.html');
await page.waitForSelector('html.mpy.py');
});

View File

@@ -0,0 +1,2 @@
[js_modules.worker]
"https://cdn.jsdelivr.net/npm/html-escaper/+esm" = "html_escaper"

View File

@@ -4,18 +4,52 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="../../dist/core.css">
<script type="module" src="../../dist/core.js"></script>
<script type="module">
import '../../dist/core.js';
addEventListener('mpy-editor', async ({ target }) => {
if (target.hasAttribute('setup')) {
await target.process([
'from pyscript import document',
// adds class="a-1" to the <html> element
'document.documentElement.classList.add(f"a-{a}")',
'from js import console',
'console.log("Hello JS")',
].join('\n'));
}
});
</script>
</head>
<body>
<script type="mpy-editor" src="task1.py" env="task1" setup></script>
<!-- a setup node with a config for an env -->
<script type="mpy-editor" src="task1.py" config="./config.toml" env="task1" setup></script>
<script type="mpy-editor" env="task1">
print(a)
from pyscript.js_modules.html_escaper import escape, unescape
print(unescape(escape("<OK>")))
a = 1
</script>
<script type="mpy-editor" env="task2" setup>
<!-- a share-nothing micropython editor -->
<script type="mpy-editor" config='{"js_modules":{"worker":{"https://cdn.jsdelivr.net/npm/html-escaper/+esm":"html_escaper"}}}'>
from pyscript.js_modules.html_escaper import escape, unescape
print(unescape(escape("<OK>")))
b = 2
try:
print(a)
except:
print("all good")
</script>
<!-- a config once micropython env -->
<script type="mpy-editor" env="task2" config="./config.toml">
from pyscript.js_modules.html_escaper import escape, unescape
print(unescape(escape("<OK>")))
c = 3
try:
print(b)
except:
print("all good")
</script>
<script type="mpy-editor" env="task2">
print(b)
print(c)
</script>
</body>
</html>

View File

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="../../dist/core.css">
<script type="module" src="../../dist/core.js"></script>
</head>
<body>
<script type="py-editor">
print("Hello!")
</script>
<script type="mpy-editor">
print("Hello!")
</script>
</body>
</html>

View File

@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="../../dist/core.css">
<script type="module" src="../../dist/core.js"></script>
</head>
<body>
<script type="mpy-editor" service-worker="./sw.js">
from pyscript import document
document.body.append("OK")
</script>
</body>
</html>

View File

@@ -0,0 +1 @@
const{isArray:e}=Array,t=new Map,s=e=>{e.stopImmediatePropagation(),e.preventDefault()};var n=Object.freeze({__proto__:null,activate:e=>e.waitUntil(clients.claim()),fetch:e=>{const{request:n}=e;"POST"===n.method&&n.url===`${location.href}?sabayon`&&(s(e),e.respondWith(n.json().then((async e=>{const{promise:s,resolve:o}=Promise.withResolvers(),a=e.join(",");t.set(a,o);for(const t of await clients.matchAll())t.postMessage(e);return s.then((e=>new Response(`[${e.join(",")}]`,n.headers)))}))))},install:()=>skipWaiting(),message:n=>{const{data:o}=n;if(e(o)&&4===o.length){const[e,a,i,r]=o,l=[e,a,i].join(",");t.has(l)&&(s(n),t.get(l)(r),t.delete(l))}}});for(const e in n)addEventListener(e,n[e]);

View File

@@ -1 +1,5 @@
from pyscript import window
window.console.log("OK")
a = 1

View File

@@ -10,6 +10,6 @@
</head>
<body>
<script type="py" src="terminal.py" worker terminal></script>
<script type="py" src="terminal.py" worker terminal></script>
<script type="mpy" src="terminal.py" worker terminal></script>
</body>
</html>

View File

@@ -9,24 +9,10 @@
<style>.xterm { padding: .5rem; }</style>
</head>
<body>
<script type="py">
def greetings(event):
print('hello world')
<script type="mpy" worker terminal>
print("µpython")
import code
code.interact()
</script>
<py-script worker terminal>
# works on both worker and main scripts
print("__terminal__", __terminal__)
import sys
from pyscript import display, document
display("Hello", "PyScript Next - PyTerminal", append=False)
print("this should go to the terminal")
print("another line")
# this works as expected
print("this goes to stderr", file=sys.stderr)
document.addEventListener('click', lambda event: print(event.type));
</py-script>
<button id="my-button" py-click="greetings">Click me</button>
</body>
</html>

View File

@@ -9,7 +9,7 @@
<style>.xterm { padding: .5rem; }</style>
</head>
<body>
<script type="py" worker terminal>
<script type="mpy" worker terminal>
from pyscript import document
document.documentElement.classList.add("first")

View File

@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<ul>
<li>
<a href="./no-repl.html">Prompt: NO REPL</a>
</li>
<li>
<a href="./repl.html">Prompt: REPL</a>
</li>
</ul>
</body>
</html>

View File

@@ -0,0 +1,20 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PyTerminal Prompt: NO REPL</title>
<script type="module" src="../../dist/core.js"></script>
<style>.xterm { padding: .5rem; }</style>
</head>
<body>
<script type="mpy" worker terminal>
prompt = input("Say something: ")
print("You said, ", prompt)
</script>
<script type="py" worker terminal>
prompt = input("Say something: ")
print("You said, ", prompt)
</script>
</body>
</html>

View File

@@ -0,0 +1,28 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PyTerminal Prompt: REPL</title>
<script type="module" src="../../dist/core.js"></script>
<style>.xterm { padding: .5rem; }</style>
</head>
<body>
<script type="mpy" worker terminal>
import code
code.interact()
prompt = input("Say something: ")
print("You said, ", prompt)
</script>
<script type="py" worker terminal>
import code
code.interact()
# Pyodide won't execute this ... ever
# this should be tested manually
prompt = input("Say something: ")
print("You said, ", prompt)
</script>
</body>
</html>

View File

@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="../dist/core.css">
<script type="module" src="../dist/core.js"></script>
</head>
<body>
<script type="py" async>
from pyscript import py_import, js_import, window
window.console.time("first")
matplotlib, regex, = await py_import("matplotlib", "regex")
window.console.timeEnd("first")
window.console.time("second")
matplotlib, regex, = await py_import("matplotlib", "regex")
window.console.timeEnd("second")
print(matplotlib, regex)
escaper, = await js_import("https://esm.run/html-escaper")
window.console.log(escaper)
</script>
</body>
</html>

View File

@@ -8,7 +8,9 @@
<script type="module" src="../dist/core.js"></script>
</head>
<body>
<script type="py" src="pydom.py"></script>
<script type="mpy" src="pydom.py"></script>
<div id="system-info"></div>
<button id="just-a-button">Click For Time</button>
<button id="color-button">Click For Color</button>

View File

@@ -1,9 +1,12 @@
import random
import sys
import time
from datetime import datetime as dt
from pyscript import display, when
from pyweb import pydom
from pyscript.web import dom
display(sys.version, target="system-info")
@when("click", "#just-a-button")
@@ -16,18 +19,15 @@ def on_click():
tstr = "{:02d}/{:02d}/{:04d} {:02d}:{:02d}:{:02d}"
timenow = tstr.format(tnow[2], tnow[1], tnow[0], *tnow[2:])
display(f"Hello from PyScript, time is: {timenow}", append=False, target="result")
display(f"Hello from PyScript, time is: {timenow}", append=False, target="#result")
@when("click", "#color-button")
def on_color_click(event):
btn = pydom["#result"]
btn = dom["#result"]
btn.style["background-color"] = f"#{random.randrange(0x1000000):06x}"
@when("click", "#color-reset-button")
def reset_color(*args, **kwargs):
pydom["#result"].style["background-color"] = "white"
# btn_reset = pydom["#color-reset-button"][0].when('click', reset_color)
dom["#result"].style["background-color"] = "white"

View File

@@ -10,6 +10,8 @@
<body>
<script type="mpy" src="pydom.py"></script>
<div id="system-info"></div>
<button id="just-a-button">Click For Time</button>
<button id="color-button">Click For Color</button>
<button id="color-reset-button">Reset Color</button>

View File

@@ -32,7 +32,7 @@
</style>
</head>
<body>
<script type="py" src="./run_tests.py" config="./tests.toml"></script>
<script type="py" src="/test/pyscript_dom/run_tests.py" config="/test/pyscript_dom/tests.toml"></script>
<h1>pyscript.dom Tests</h1>
<p>You can pass test parameters to this test suite by passing them as query params on the url.

View File

@@ -1,23 +1,11 @@
from unittest import mock
import pytest
from pyscript import document, when
from pyweb import pydom
from pyscript.web import Element, ElementCollection, div, p, page
class TestDocument:
def test__element(self):
assert pydom._js == document
def test_no_parent(self):
assert pydom.parent is None
def test_create_element(self):
new_el = pydom.create("div")
assert isinstance(new_el, pydom.BaseElement)
assert new_el._js.tagName == "DIV"
# EXPECT the new element to be associated with the document
assert new_el.parent == None
assert page.body._dom_element == document.body
assert page.head._dom_element == document.head
def test_getitem_by_id():
@@ -26,14 +14,14 @@ def test_getitem_by_id():
txt = "You found test_id_selector"
selector = f"#{id_}"
# EXPECT the element to be found by id
result = pydom[selector]
result = page.find(selector)
div = result[0]
# EXPECT the element text value to match what we expect and what
# the JS document.querySelector API would return
assert document.querySelector(selector).innerHTML == div.html == txt
assert document.querySelector(selector).innerHTML == div.innerHTML == txt
# EXPECT the results to be of the right types
assert isinstance(div, pydom.BaseElement)
assert isinstance(result, pydom.ElementCollection)
assert isinstance(div, Element)
assert isinstance(result, ElementCollection)
def test_getitem_by_class():
@@ -43,8 +31,7 @@ def test_getitem_by_class():
"test_selector_w_children_child_1",
]
expected_class = "a-test-class"
result = pydom[f".{expected_class}"]
div = result[0]
result = page.find(f".{expected_class}")
# EXPECT to find exact number of elements with the class in the page (== 3)
assert len(result) == 3
@@ -54,40 +41,40 @@ def test_getitem_by_class():
def test_read_n_write_collection_elements():
elements = pydom[".multi-elems"]
elements = page.find(".multi-elems")
for element in elements:
assert element.html == f"Content {element.id.replace('#', '')}"
assert element.innerHTML == f"Content {element.id.replace('#', '')}"
new_content = "New Content"
elements.html = new_content
elements.innerHTML = new_content
for element in elements:
assert element.html == new_content
assert element.innerHTML == new_content
class TestElement:
def test_query(self):
# GIVEN an existing element on the page, with at least 1 child element
id_ = "test_selector_w_children"
parent_div = pydom[f"#{id_}"][0]
parent_div = page.find(f"#{id_}")[0]
# EXPECT it to be able to query for the first child element
div = parent_div.find("div")[0]
# EXPECT the new element to be associated with the parent
assert div.parent == parent_div
# EXPECT the new element to be a BaseElement
assert isinstance(div, pydom.BaseElement)
# EXPECT the new element to be an Element
assert isinstance(div, Element)
# EXPECT the div attributes to be == to how they are configured in the page
assert div.html == "Child 1"
assert div.innerHTML == "Child 1"
assert div.id == "test_selector_w_children_child_1"
def test_equality(self):
# GIVEN 2 different Elements pointing to the same underlying element
id_ = "test_id_selector"
selector = f"#{id_}"
div = pydom[selector][0]
div2 = pydom[selector][0]
div = page.find(selector)[0]
div2 = page.find(selector)[0]
# EXPECT them to be equal
assert div == div2
@@ -95,34 +82,34 @@ class TestElement:
assert div is not div2
# EXPECT their value to always be equal
assert div.html == div2.html
div.html = "some value"
assert div.innerHTML == div2.innerHTML
div.innerHTML = "some value"
assert div.html == div2.html == "some value"
assert div.innerHTML == div2.innerHTML == "some value"
def test_append_element(self):
id_ = "element-append-tests"
div = pydom[f"#{id_}"][0]
div = page.find(f"#{id_}")[0]
len_children_before = len(div.children)
new_el = div.create("p")
new_el = p("new element")
div.append(new_el)
assert len(div.children) == len_children_before + 1
assert div.children[-1] == new_el
def test_append_js_element(self):
def test_append_dom_element_element(self):
id_ = "element-append-tests"
div = pydom[f"#{id_}"][0]
div = page.find(f"#{id_}")[0]
len_children_before = len(div.children)
new_el = div.create("p")
div.append(new_el._js)
new_el = p("new element")
div.append(new_el._dom_element)
assert len(div.children) == len_children_before + 1
assert div.children[-1] == new_el
def test_append_collection(self):
id_ = "element-append-tests"
div = pydom[f"#{id_}"][0]
div = page.find(f"#{id_}")[0]
len_children_before = len(div.children)
collection = pydom[".collection"]
collection = page.find(".collection")
div.append(collection)
assert len(div.children) == len_children_before + len(collection)
@@ -132,24 +119,24 @@ class TestElement:
def test_read_classes(self):
id_ = "test_class_selector"
expected_class = "a-test-class"
div = pydom[f"#{id_}"][0]
div = page.find(f"#{id_}")[0]
assert div.classes == [expected_class]
def test_add_remove_class(self):
id_ = "div-no-classes"
classname = "tester-class"
div = pydom[f"#{id_}"][0]
div = page.find(f"#{id_}")[0]
assert not div.classes
div.add_class(classname)
same_div = pydom[f"#{id_}"][0]
div.classes.add(classname)
same_div = page.find(f"#{id_}")[0]
assert div.classes == [classname] == same_div.classes
div.remove_class(classname)
div.classes.remove(classname)
assert div.classes == [] == same_div.classes
def test_when_decorator(self):
called = False
just_a_button = pydom["#a-test-button"][0]
just_a_button = page.find("#a-test-button")[0]
@when("click", just_a_button)
def on_click(event):
@@ -157,45 +144,49 @@ class TestElement:
called = True
# Now let's simulate a click on the button (using the low level JS API)
# so we don't risk pydom getting in the way
# so we don't risk dom getting in the way
assert not called
just_a_button._js.click()
just_a_button._dom_element.click()
assert called
def test_html_attribute(self):
def test_inner_html_attribute(self):
# GIVEN an existing element on the page with a known empty text content
div = pydom["#element_attribute_tests"][0]
div = page.find("#element_attribute_tests")[0]
# WHEN we set the html attribute
div.html = "<b>New Content</b>"
div.innerHTML = "<b>New Content</b>"
# EXPECT the element html and underlying JS Element innerHTML property
# to match what we expect and what
assert div.html == div._js.innerHTML == "<b>New Content</b>"
assert div.text == div._js.textContent == "New Content"
assert div.innerHTML == div._dom_element.innerHTML == "<b>New Content</b>"
assert div.textContent == div._dom_element.textContent == "New Content"
def test_text_attribute(self):
# GIVEN an existing element on the page with a known empty text content
div = pydom["#element_attribute_tests"][0]
div = page.find("#element_attribute_tests")[0]
# WHEN we set the html attribute
div.text = "<b>New Content</b>"
div.textContent = "<b>New Content</b>"
# EXPECT the element html and underlying JS Element innerHTML property
# to match what we expect and what
assert div.html == div._js.innerHTML == "&lt;b&gt;New Content&lt;/b&gt;"
assert div.text == div._js.textContent == "<b>New Content</b>"
assert (
div.innerHTML
== div._dom_element.innerHTML
== "&lt;b&gt;New Content&lt;/b&gt;"
)
assert div.textContent == div._dom_element.textContent == "<b>New Content</b>"
class TestCollection:
def test_iter_eq_children(self):
elements = pydom[".multi-elems"]
assert [el for el in elements] == [el for el in elements.children]
elements = page.find(".multi-elems")
assert [el for el in elements] == [el for el in elements.elements]
assert len(elements) == 3
def test_slices(self):
elements = pydom[".multi-elems"]
elements = page.find(".multi-elems")
assert elements[0]
_slice = elements[:2]
assert len(_slice) == 2
@@ -205,26 +196,26 @@ class TestCollection:
def test_style_rule(self):
selector = ".multi-elems"
elements = pydom[selector]
elements = page.find(selector)
for el in elements:
assert el.style["background-color"] != "red"
elements.style["background-color"] = "red"
for i, el in enumerate(pydom[selector]):
for i, el in enumerate(page.find(selector)):
assert elements[i].style["background-color"] == "red"
assert el.style["background-color"] == "red"
elements.style.remove("background-color")
for i, el in enumerate(pydom[selector]):
for i, el in enumerate(page.find(selector)):
assert el.style["background-color"] != "red"
assert elements[i].style["background-color"] != "red"
def test_when_decorator(self):
called = False
buttons_collection = pydom["button"]
buttons_collection = page.find("button")
@when("click", buttons_collection)
def on_click(event):
@@ -232,42 +223,43 @@ class TestCollection:
called = True
# Now let's simulate a click on the button (using the low level JS API)
# so we don't risk pydom getting in the way
# so we don't risk dom getting in the way
assert not called
for button in buttons_collection:
button._js.click()
button._dom_element.click()
assert called
called = False
class TestCreation:
def test_create_document_element(self):
new_el = pydom.create("div")
# TODO: This test should probably be removed since it's testing the elements
# module.
new_el = div("new element")
new_el.id = "new_el_id"
assert isinstance(new_el, pydom.BaseElement)
assert new_el._js.tagName == "DIV"
assert isinstance(new_el, Element)
assert new_el._dom_element.tagName == "DIV"
# EXPECT the new element to be associated with the document
assert new_el.parent == None
pydom.body.append(new_el)
assert new_el.parent is None
page.body.append(new_el)
assert pydom["#new_el_id"][0].parent == pydom.body
assert page.find("#new_el_id")[0].parent == page.body
def test_create_element_child(self):
selector = "#element-creation-test"
parent_div = pydom[selector][0]
parent_div = page.find(selector)[0]
# Creating an element from another element automatically creates that element
# as a child of the original element
new_el = parent_div.create(
"p", classes=["code-description"], html="Ciao PyScripters!"
)
new_el = p("a div", classes=["code-description"], innerHTML="Ciao PyScripters!")
parent_div.append(new_el)
assert isinstance(new_el, Element)
assert new_el._dom_element.tagName == "P"
assert isinstance(new_el, pydom.BaseElement)
assert new_el._js.tagName == "P"
# EXPECT the new element to be associated with the document
assert new_el.parent == parent_div
assert pydom[selector][0].children[0] == new_el
assert page.find(selector)[0].children[0] == new_el
class TestInput:
@@ -281,10 +273,10 @@ class TestInput:
def test_value(self):
for id_ in self.input_ids:
expected_type = id_.split("_")[-1]
result = pydom[f"#{id_}"]
result = page.find(f"#{id_}")
input_el = result[0]
assert input_el._js.type == expected_type
assert input_el.value == f"Content {id_}" == input_el._js.value
assert input_el._dom_element.type == expected_type
assert input_el.value == f"Content {id_}" == input_el._dom_element.value
# Check that we can set the value
new_value = f"New Value {expected_type}"
@@ -299,7 +291,7 @@ class TestInput:
def test_set_value_collection(self):
for id_ in self.input_ids:
input_el = pydom[f"#{id_}"]
input_el = page.find(f"#{id_}")
assert input_el.value[0] == f"Content {id_}" == input_el[0].value
@@ -307,36 +299,35 @@ class TestInput:
input_el.value = new_value
assert input_el.value[0] == new_value == input_el[0].value
def test_element_without_value(self):
result = pydom[f"#tests-terminal"][0]
with pytest.raises(AttributeError):
result.value = "some value"
def test_element_without_collection(self):
result = pydom[f"#tests-terminal"]
with pytest.raises(AttributeError):
result.value = "some value"
def test_element_without_collection(self):
result = pydom[f"#tests-terminal"]
with pytest.raises(AttributeError):
result.value = "some value"
# TODO: We only attach attributes to the classes that have them now which means we
# would have to have some other way to help users if using attributes that aren't
# actually on the class. Maybe a job for __setattr__?
#
# def test_element_without_value(self):
# result = page.find(f"#tests-terminal"][0]
# with pytest.raises(AttributeError):
# result.value = "some value"
#
# def test_element_without_value_via_collection(self):
# result = page.find(f"#tests-terminal"]
# with pytest.raises(AttributeError):
# result.value = "some value"
class TestSelect:
def test_select_options_iter(self):
select = pydom[f"#test_select_element_w_options"][0]
select = page.find(f"#test_select_element_w_options")[0]
for i, option in enumerate(select.options, 1):
assert option.value == f"{i}"
assert option.html == f"Option {i}"
assert option.innerHTML == f"Option {i}"
def test_select_options_len(self):
select = pydom[f"#test_select_element_w_options"][0]
select = page.find(f"#test_select_element_w_options")[0]
assert len(select.options) == 2
def test_select_options_clear(self):
select = pydom[f"#test_select_element_to_clear"][0]
select = page.find(f"#test_select_element_to_clear")[0]
assert len(select.options) == 3
select.options.clear()
@@ -345,7 +336,7 @@ class TestSelect:
def test_select_element_add(self):
# GIVEN the existing select element with no options
select = pydom[f"#test_select_element"][0]
select = page.find(f"#test_select_element")[0]
# EXPECT the select element to have no options
assert len(select.options) == 0
@@ -357,7 +348,7 @@ class TestSelect:
# we passed in
assert len(select.options) == 1
assert select.options[0].value == "1"
assert select.options[0].html == "Option 1"
assert select.options[0].innerHTML == "Option 1"
# WHEN we add another option (blank this time)
select.options.add("")
@@ -367,7 +358,7 @@ class TestSelect:
# EXPECT the last option to have an empty value and html
assert select.options[1].value == ""
assert select.options[1].html == ""
assert select.options[1].innerHTML == ""
# WHEN we add another option (this time adding it in between the other 2
# options by using an integer index)
@@ -378,11 +369,11 @@ class TestSelect:
# EXPECT the middle option to have the value and html we passed in
assert select.options[0].value == "1"
assert select.options[0].html == "Option 1"
assert select.options[0].innerHTML == "Option 1"
assert select.options[1].value == "2"
assert select.options[1].html == "Option 2"
assert select.options[1].innerHTML == "Option 2"
assert select.options[2].value == ""
assert select.options[2].html == ""
assert select.options[2].innerHTML == ""
# WHEN we add another option (this time adding it in between the other 2
# options but using the option itself)
@@ -395,38 +386,48 @@ class TestSelect:
# EXPECT the middle option to have the value and html we passed in
assert select.options[0].value == "1"
assert select.options[0].html == "Option 1"
assert select.options[0].selected == select.options[0]._js.selected == False
assert select.options[0].innerHTML == "Option 1"
assert (
select.options[0].selected
== select.options[0]._dom_element.selected
== False
)
assert select.options[1].value == "2"
assert select.options[1].html == "Option 2"
assert select.options[1].innerHTML == "Option 2"
assert select.options[2].value == "3"
assert select.options[2].html == "Option 3"
assert select.options[2].selected == select.options[2]._js.selected == True
assert select.options[2].innerHTML == "Option 3"
assert (
select.options[2].selected
== select.options[2]._dom_element.selected
== True
)
assert select.options[3].value == ""
assert select.options[3].html == ""
assert select.options[3].innerHTML == ""
# WHEN we add another option (this time adding it in between the other 2
# options but using the JS element of the option itself)
select.options.add(value="2a", html="Option 2a", before=select.options[2]._js)
select.options.add(
value="2a", html="Option 2a", before=select.options[2]._dom_element
)
# EXPECT the select element to have 3 options
assert len(select.options) == 5
# EXPECT the middle option to have the value and html we passed in
assert select.options[0].value == "1"
assert select.options[0].html == "Option 1"
assert select.options[0].innerHTML == "Option 1"
assert select.options[1].value == "2"
assert select.options[1].html == "Option 2"
assert select.options[1].innerHTML == "Option 2"
assert select.options[2].value == "2a"
assert select.options[2].html == "Option 2a"
assert select.options[2].innerHTML == "Option 2a"
assert select.options[3].value == "3"
assert select.options[3].html == "Option 3"
assert select.options[3].innerHTML == "Option 3"
assert select.options[4].value == ""
assert select.options[4].html == ""
assert select.options[4].innerHTML == ""
def test_select_options_remove(self):
# GIVEN the existing select element with 3 options
select = pydom[f"#test_select_element_to_remove"][0]
select = page.find(f"#test_select_element_to_remove")[0]
# EXPECT the select element to have 3 options
assert len(select.options) == 4
@@ -448,12 +449,12 @@ class TestSelect:
def test_select_get_selected_option(self):
# GIVEN the existing select element with one selected option
select = pydom[f"#test_select_element_w_options"][0]
select = page.find(f"#test_select_element_w_options")[0]
# WHEN we get the selected option
selected_option = select.options.selected
# EXPECT the selected option to be correct
assert selected_option.value == "2"
assert selected_option.html == "Option 2"
assert selected_option.selected == selected_option._js.selected == True
assert selected_option.innerHTML == "Option 2"
assert selected_option.selected == selected_option._dom_element.selected == True

View File

@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Service Worker</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="../../dist/core.css">
<script type="module" src="../../dist/core.js"></script>
</head>
<body>
<script type="mpy" service-worker="./sabayon.js" worker>
from pyscript import document
document.body.append('OK')
</script>
</body>
</html>

View File

@@ -0,0 +1,28 @@
/*! coi-serviceworker v0.1.7 - Guido Zuidhof and contributors, licensed under MIT */
/*! mini-coi - Andrea Giammarchi and contributors, licensed under MIT */
(({ document: d, navigator: { serviceWorker: s } }) => {
if (d) {
const { currentScript: c } = d;
s.register(c.src, { scope: c.getAttribute('scope') || '.' }).then(r => {
r.addEventListener('updatefound', () => location.reload());
if (r.active && !s.controller) location.reload();
});
}
else {
addEventListener('install', () => skipWaiting());
addEventListener('activate', e => e.waitUntil(clients.claim()));
addEventListener('fetch', e => {
const { request: r } = e;
if (r.cache === 'only-if-cached' && r.mode !== 'same-origin') return;
e.respondWith(fetch(r).then(r => {
const { body, status, statusText } = r;
if (!status || status > 399) return r;
const h = new Headers(r.headers);
h.set('Cross-Origin-Opener-Policy', 'same-origin');
h.set('Cross-Origin-Embedder-Policy', 'require-corp');
h.set('Cross-Origin-Resource-Policy', 'cross-origin');
return new Response(body, { status, statusText, headers: h });
}));
});
}
})(self);

View File

@@ -0,0 +1 @@
const{isArray:e}=Array,t=new Map,s=e=>{e.stopImmediatePropagation(),e.preventDefault()};var n=Object.freeze({__proto__:null,activate:e=>e.waitUntil(clients.claim()),fetch:e=>{const{request:n}=e;"POST"===n.method&&n.url===`${location.href}?sabayon`&&(s(e),e.respondWith(n.json().then((async e=>{const{promise:s,resolve:o}=Promise.withResolvers(),a=e.join(",");t.set(a,o);for(const t of await clients.matchAll())t.postMessage(e);return s.then((e=>new Response(`[${e.join(",")}]`,n.headers)))}))))},install:()=>skipWaiting(),message:n=>{const{data:o}=n;if(e(o)&&4===o.length){const[e,a,i,r]=o,l=[e,a,i].join(",");t.has(l)&&(s(n),t.get(l)(r),t.delete(l))}}});for(const e in n)addEventListener(e,n[e]);

View File

@@ -0,0 +1,46 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@pyscript/core storage</title>
<link rel="stylesheet" href="../dist/core.css">
<script type="module" src="../dist/core.js"></script>
</head>
<body>
<script type="mpy" async>
from random import random
from pyscript import storage
store = await storage(name="test")
print("before", len(store))
for k in store:
if isinstance(store[k], memoryview):
print(f" {k}: {store[k].hex()} as hex()")
else:
print(f" {k}: {store[k]}")
store["ba"] = bytearray([0, 1, 2, 3, 4])
store["mv"] = memoryview(bytearray([5, 6, 7, 8, 9]))
store["random"] = ("some", random(), True)
store["key"] = "value"
print("now", len(store))
for k in store:
print(f" {k}: {store[k]}")
del store["key"]
# store.clear()
print("after", len(store))
for k in store:
print(f" {k}: {store[k]}")
await store.sync()
import js
js.document.documentElement.classList.add("ok")
</script>
</body>
</html>

View File

@@ -0,0 +1,251 @@
try:
from textwrap import dedent
except ImportError:
dedent = lambda x: x
import examples
import shoelace
import styles
from markdown import markdown
from pyscript import when, window
from pyweb import pydom
from pyweb.ui import elements as el
from pyweb.ui.elements import a, button, div, grid, h1, h2, h3
MAIN_PAGE_MARKDOWN = dedent(
"""
## What is pyweb.ui?
Pyweb UI is a totally immagnary exercise atm but..... imagine it is a Python library that allows you to create
web applications using Python only.
It is based on base HTML/JS components but is extensible, for instance, it can have a [Shoelace](https://shoelace.style/) backend...
PyWeb is a Python library that allows you to create web applications using Python only.
## What can I do with Pyweb.ui?
You can create web applications using Python only.
"""
)
# First thing we do is to load all the external resources we need
shoelace.load_resources()
# Let's define some convenience functions first
def create_component_details(component_label, component):
"""Create a component details card.
Args:
component (str): The name of the component to create.
Returns:
the component created
"""
# Get the example from the examples catalog
example = component["instance"]
details = (
getattr(example, "__doc__", "")
or f"Details missing for component {component_label}"
)
return div(
[
# Title and description (description is picked from the class docstring)
h1(component_label),
markdown(details),
# Example section
h2("Example:"),
create_component_example(component["instance"], component["code"]),
],
style={"margin": "20px"},
)
def add_component_section(component_label, component, parent_div):
"""Create a link to a component and add it to the left panel.
Args:
component (str): The name of the component to add.
Returns:
the component created
"""
# Create the component link element
div_ = div(
a(component_label, href="#"),
style={"display": "block", "text-align": "center", "margin": "auto"},
)
# Create a handler that opens the component details when the link is clicked
@when("click", div_)
def _change():
new_main = create_component_details(component_label, component)
main_area.html = ""
main_area.append(new_main)
# Add the new link element to the parent div (left panel)
parent_div.append(div_)
return div_
def create_component_example(widget, code):
"""Create a grid div with the widget on the left side and the relate code
on the right side.
Args:
widget (ElementBase): The widget to add to the grid.
code (str): The code to add to the grid.
Returns:
the grid created
"""
# Create the grid that splits the window in two columns (25% and 75%)
grid_ = grid("29% 2% 74%")
# Add the widget
grid_.append(div(widget, style=styles.STYLE_EXAMPLE_INSTANCE))
# Add the code div
widget_code = markdown(dedent(f"""```python\n{code}\n```"""))
grid_.append(shoelace.Divider(vertical=True))
grid_.append(div(widget_code, style=styles.STYLE_CODE_BLOCK))
return grid_
def create_main_area():
"""Create the main area of the right side of page, with the description of the
demo itself and how to use it.
Returns:
the main area
"""
div_ = div(
[
h1("Welcome to PyWeb UI!", style={"text-align": "center"}),
markdown(MAIN_PAGE_MARKDOWN),
]
)
main = el.main(
style={
"padding-top": "4rem",
"padding-bottom": "7rem",
"max-width": "52rem",
"margin-left": "auto",
"margin-right": "auto",
"padding-left": "1.5rem",
"padding-right": "1.5rem",
"width": "100%",
}
)
main.append(div_)
return main
def create_basic_components_page(label, kit_name):
"""Create the basic components page.
Returns:
the main area
"""
div_ = div(h2(label))
for component_label, component in examples.kits[kit_name].items():
div_.append(h3(component_label))
div_.append(create_component_example(component["instance"], component["code"]))
return div_
# ********** CREATE ALL THE LAYOUT **********
main_grid = grid("140px 20px auto", style={"min-height": "100%"})
# ********** MAIN PANEL **********
main_area = create_main_area()
def write_to_main(content):
main_area.html = ""
main_area.append(content)
def restore_home():
write_to_main(create_main_area())
def basic_components():
write_to_main(
create_basic_components_page(label="Basic Components", kit_name="elements")
)
# Make sure we highlight the code
window.hljs.highlightAll()
def markdown_components():
write_to_main(create_basic_components_page(label="", kit_name="markdown"))
def create_new_section(title, parent_div):
basic_components_text = h3(
title, style={"text-align": "left", "margin": "20px auto 0"}
)
parent_div.append(basic_components_text)
parent_div.append(
shoelace.Divider(style={"margin-top": "5px", "margin-bottom": "30px"})
)
return basic_components_text
# ********** LEFT PANEL **********
left_div = div()
left_panel_title = h1(
"PyWeb.UI", style={"text-align": "center", "margin": "20px auto 30px"}
)
left_div.append(left_panel_title)
left_div.append(shoelace.Divider(style={"margin-bottom": "30px"}))
# Let's map the creation of the main area to when the user clocks on "Components"
when("click", left_panel_title)(restore_home)
# BASIC COMPONENTS
basic_components_text = h3(
"Basic Components",
style={"text-align": "left", "margin": "20px auto 0", "cursor": "pointer"},
)
left_div.append(basic_components_text)
left_div.append(shoelace.Divider(style={"margin-top": "5px", "margin-bottom": "30px"}))
# Let's map the creation of the main area to when the user clocks on "Components"
when("click", basic_components_text)(basic_components)
# MARKDOWN COMPONENTS
markdown_title = create_new_section("Markdown", left_div)
when("click", markdown_title)(markdown_components)
# SHOELACE COMPONENTS
shoe_components_text = h3(
"Shoe Components", style={"text-align": "left", "margin": "20px auto 0"}
)
left_div.append(shoe_components_text)
left_div.append(shoelace.Divider(style={"margin-top": "5px", "margin-bottom": "30px"}))
# Create the links to the components on th left panel
print("SHOELACE EXAMPLES", examples.kits["shoelace"])
for component_label, component in examples.kits["shoelace"].items():
add_component_section(component_label, component, left_div)
left_div.append(shoelace.Divider(style={"margin-top": "5px", "margin-bottom": "30px"}))
left_div.append(a("Gallery", href="gallery.html", style={"text-align": "left"}))
# ********** ADD LEFT AND MAIN PANEL TO MAIN **********
main_grid.append(left_div)
main_grid.append(shoelace.Divider(vertical=True))
main_grid.append(main_area)
pydom.body.append(main_grid)

View File

@@ -0,0 +1,300 @@
from markdown import markdown
from pyscript import when, window
from pyweb import pydom
from pyweb.ui.elements import (
a,
br,
button,
code,
div,
grid,
h1,
h2,
h3,
h4,
h5,
h6,
img,
input_,
p,
small,
strong,
)
from shoelace import (
Alert,
Button,
Card,
CopyButton,
Details,
Dialog,
Divider,
Icon,
Radio,
RadioGroup,
Range,
Rating,
RelativeTime,
Skeleton,
Spinner,
Switch,
Tag,
Textarea,
)
LOREM_IPSUM = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
details_code = """
LOREM_IPSUM = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
Details(LOREM_IPSUM, summary="Try me")
"""
example_dialog_close_btn = Button("Close")
example_dialog = Dialog(div([p(LOREM_IPSUM), example_dialog_close_btn]), label="Try me")
example_dialog_btn = Button("Open Dialog")
def toggle_dialog():
example_dialog.open = not (example_dialog.open)
when("click", example_dialog_btn)(toggle_dialog)
when("click", example_dialog_close_btn)(toggle_dialog)
pydom.body.append(example_dialog)
# ELEMENTS
# Button
btn = button("Click me!")
when("click", btn)(lambda: window.alert("Clicked!"))
# Inputs
inputs_div = div()
inputs_code = []
for input_type in [
"text",
"password",
"email",
"number",
"date",
"time",
"color",
"range",
]:
inputs_div.append(input_(type=input_type, style={"display": "block"}))
inputs_code.append(f"input_(type='{input_type}')")
headers_div = div()
headers_code = []
for header in [h1, h2, h3, h4, h5, h6]:
headers_div.append(header(f"{header.tag.upper()} header"))
headers_code.append(f'{header.tag}("{header.tag.upper()} header")')
headers_code = "\n".join(headers_code)
rich_input = input_(
type="text",
name="some name",
autofocus=True,
pattern="\w{3,16}",
placeholder="add text with > 3 chars",
required=True,
size="20",
)
inputs_div.append(rich_input)
inputs_code.append("# You can create inputs with more options like")
inputs_code.append("# this by passing properties as kwargs")
inputs_code.append(
"input_(type='text', name='some name', autofocus=True, pattern='\\w{3,16}', placeholder='add text with > 3 chars', required=True, size='20')"
)
inputs_code = "\n".join(inputs_code)
MARKDOWN_EXAMPLE = """# This is a header
This is a ~~paragraph~~ text with **bold** and *italic* text in it!
"""
kits = {
"shoelace": {
"Alert": {
"instance": Alert(
"This is a standard alert. You can customize its content and even the icon."
),
"code": "Alert('This is a standard alert. You can customize its content and even the icon.'",
},
"Icon": {
"instance": Icon(name="heart"),
"code": 'Icon(name="heart")',
},
"Button": {
"instance": Button("Try me"),
"code": 'Button("Try me")',
},
"Card": {
"instance": Card(
p("This is a cool card!"),
image="https://pyscript.net/assets/images/pyscript-sticker-black.svg",
footer=div([Button("More Info"), Rating()]),
),
"code": """
Card(p("This is a cool card!"), image="https://pyscript.net/assets/images/pyscript-sticker-black.svg", footer=div([Button("More Info"), Rating()]))
""",
},
"Details": {
"instance": Details(LOREM_IPSUM, summary="Try me"),
"code": 'Details(LOREM_IPSUM, summary="Try me")',
},
"Dialog": {
"instance": example_dialog_btn,
"code": 'Dialog(div([p(LOREM_IPSUM), Button("Close")]), summary="Try me")',
},
"Divider": {
"instance": Divider(),
"code": "Divider()",
},
"Rating": {
"instance": Rating(),
"code": "Rating()",
},
"Radio": {
"instance": Radio("Option 42"),
"code": code('Radio("Option 42")'),
},
"Radio Group": {
"instance": RadioGroup(
[
Radio("radio 1", name="radio 1", value=1, style={"margin": "20px"}),
Radio("radio 2", name="radio 2", value=2, style={"margin": "20px"}),
Radio("radio 3", name="radio 3", value=3, style={"margin": "20px"}),
],
label="Select an option",
),
"code": code(
"""
RadioGroup([Radio("radio 1", name="radio 1", value=1, style={"margin": "20px"}),
Radio("radio 2", name="radio 2", value=2, style={"margin": "20px"}),
Radio("radio 3", name="radio 3", value=3, style={"margin": "20px"})],
label="Select an option"),"""
),
},
"CopyButton": {
"instance": CopyButton(
value="PyShoes!",
copy_label="Copy me!",
sucess_label="Copied, check your clipboard!",
error_label="Oops, something went wrong!",
feedback_timeout=2000,
tooltip_placement="top",
),
"code": 'CopyButton(value="PyShoes!", copy_label="Copy me!", sucess_label="Copied, check your clipboard!", error_label="Oops, something went wrong!", feedback_timeout=2000, tooltip_placement="top")',
},
"Skeleton": {
"instance": Skeleton(effect="pulse"),
"code": "Skeleton(effect='pulse')",
},
"Spinner": {
"instance": Spinner(),
"code": "Spinner()",
},
"Switch": {
"instance": Switch(name="switch", size="large"),
"code": 'Switch(name="switch", size="large")',
},
"Textarea": {
"instance": Textarea(
name="textarea",
label="Textarea",
size="medium",
help_text="This is a textarea",
resize="auto",
),
"code": 'Textarea(name="textarea", label="Textarea", size="medium", help_text="This is a textarea", resize="auto")',
},
"Tag": {
"instance": Tag("Tag", variant="primary", size="medium"),
"code": 'Tag("Tag", variant="primary", size="medium")',
},
"Range": {
"instance": Range(min=0, max=100, value=50),
"code": "Range(min=0, max=100, value=50)",
},
"RelativeTime": {
"instance": RelativeTime(date="2021-01-01T00:00:00Z"),
"code": 'RelativeTime(date="2021-01-01T00:00:00Z")',
},
# "SplitPanel": {
# "instance": SplitPanel(
# div("First panel"), div("Second panel"), orientation="vertical"
# ),
# "code": code(
# 'SplitPanel(div("First panel"), div("Second panel"), orientation="vertical")'
# ),
# },
},
"elements": {
"button": {
"instance": btn,
"code": """btn = button("Click me!")
when('click', btn)(lambda: window.alert("Clicked!"))
parentdiv.append(btn)
""",
},
"div": {
"instance": div(
"This is a div",
style={
"text-align": "center",
"margin": "0 auto",
"background-color": "cornsilk",
},
),
"code": 'div("This is a div", style={"text-align": "center", "margin": "0 auto", "background-color": "cornsilk"})',
},
"input": {"instance": inputs_div, "code": inputs_code},
"grid": {
"instance": grid(
"30% 70%",
[
div("This is a grid", style={"background-color": "lightblue"}),
p("with 2 elements", style={"background-color": "lightyellow"}),
],
),
"code": 'grid([div("This is a grid")])',
},
"headers": {"instance": headers_div, "code": headers_code},
"a": {
"instance": a(
"Click here for something awesome",
href="https://pyscript.net",
target="_blank",
),
"code": 'a("Click here for something awesome", href="https://pyscript.net", target="_blank")',
},
"br": {
"instance": div([p("This is a paragraph"), br(), p("with a line break")]),
"code": 'div([p("This is a paragraph"), br(), p("with a line break")])',
},
"img": {
"instance": img(src="./giphy_winner.gif", style={"max-width": "200px"}),
"code": 'img(src="./giphy_winner.gif", style={"max-width": "200px"})',
},
"code": {
"instance": code("print('Hello, World!')"),
"code": "code(\"print('Hello, World!')\")",
},
"p": {"instance": p("This is a paragraph"), "code": 'p("This is a paragraph")'},
"small": {
"instance": small("This is a small text"),
"code": 'small("This is a small text")',
},
"strong": {
"instance": strong("This is a strong text"),
"code": 'strong("This is a strong text")',
},
},
"markdown": {
"markdown": {
"instance": markdown(MARKDOWN_EXAMPLE),
"code": f'markdown("""{MARKDOWN_EXAMPLE}""")',
},
},
}

View File

@@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>PyDom UI</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="../../dist/core.css">
<script type="module" src="../../dist/core.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked@11.1.1/lib/marked.umd.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<!-- and it's easy to individually load additional languages -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/go.min.js"></script>
<script>hljs.highlightAll();</script>
<style>
body {
font-family: -apple-system, "system-ui", "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"
}
</style>
</head>
<body>
<script type="mpy" src="./gallery.py" config="./pyscript.toml"></script>
</body>
</html>

View File

@@ -0,0 +1,180 @@
try:
from textwrap import dedent
except ImportError:
dedent = lambda x: x
import inspect
import shoelace
import styles
import tictactoe
from markdown import markdown
from pyscript import when, window
from pyweb import pydom
from pyweb.ui import elements as el
MAIN_PAGE_MARKDOWN = dedent(
"""
This gallery is a collection of demos using the PyWeb.UI library. There are meant
to be examples of how to use the library to create GUI applications using Python
only.
## How to use the gallery
Simply click on the demo you want to see and the details will appear on the right
"""
)
# First thing we do is to load all the external resources we need
shoelace.load_resources()
def add_demo(demo_name, demo_creator_cb, parent_div, source=None):
"""Create a link to a component and add it to the left panel.
Args:
component (str): The name of the component to add.
Returns:
the component created
"""
# Create the component link element
div = el.div(el.a(demo_name, href="#"), style=styles.STYLE_LEFT_PANEL_LINKS)
# Create a handler that opens the component details when the link is clicked
@when("click", div)
def _change():
if source:
demo_div = el.grid("50% 50%")
demo_div.append(demo_creator_cb())
widget_code = markdown(dedent(f"""```python\n{source}\n```"""))
demo_div.append(el.div(widget_code, style=styles.STYLE_CODE_BLOCK))
else:
demo_div = demo_creator_cb()
demo_div.style["margin"] = "20px"
write_to_main(demo_div)
window.hljs.highlightAll()
# Add the new link element to the parent div (left panel)
parent_div.append(div)
return div
def create_main_area():
"""Create the main area of the right side of page, with the description of the
demo itself and how to use it.
Returns:
the main area
"""
return el.div(
[
el.h1("PyWeb UI Gallery", style={"text-align": "center"}),
markdown(MAIN_PAGE_MARKDOWN),
]
)
def create_markdown_app():
"""Create the basic components page.
Returns:
the main area
"""
translate_button = shoelace.Button("Convert", variant="primary")
markdown_txt_area = shoelace.TextArea(label="Use this to write your Markdown")
result_div = el.div(style=styles.STYLE_MARKDOWN_RESULT)
@when("click", translate_button)
def translate_markdown():
result_div.html = markdown(markdown_txt_area.value).html
return el.div(
[
el.h2("Markdown"),
markdown_txt_area,
translate_button,
result_div,
],
style={"margin": "20px"},
)
# ********** MAIN PANEL **********
main_area = create_main_area()
def write_to_main(content):
main_area.html = ""
main_area.append(content)
def restore_home():
write_to_main(create_main_area())
def create_new_section(title, parent_div):
basic_components_text = el.h3(
title, style={"text-align": "left", "margin": "20px auto 0"}
)
parent_div.append(basic_components_text)
parent_div.append(
shoelace.Divider(style={"margin-top": "5px", "margin-bottom": "30px"})
)
return basic_components_text
# ********** LEFT PANEL **********
left_panel_title = el.h1("PyWeb.UI", style=styles.STYLE_LEFT_PANEL_TITLE)
left_div = el.div(
[
left_panel_title,
shoelace.Divider(style={"margin-bottom": "30px"}),
el.h3("Demos", style=styles.STYLE_LEFT_PANEL_TITLE),
]
)
# Let's map the creation of the main area to when the user clocks on "Components"
when("click", left_panel_title)(restore_home)
# ------ ADD DEMOS ------
markdown_source = """
translate_button = shoelace.Button("Convert", variant="primary")
markdown_txt_area = shoelace.TextArea(label="Markdown",
help_text="Write your Mardown here and press convert to see the result",
)
result_div = el.div(style=styles.STYLE_MARKDOWN_RESULT)
@when("click", translate_button)
def translate_markdown():
result_div.html = markdown(markdown_txt_area.value).html
el.div([
el.h2("Markdown"),
markdown_txt_area,
translate_button,
result_div,
])
"""
add_demo("Markdown", create_markdown_app, left_div, source=markdown_source)
add_demo(
"Tic Tac Toe",
tictactoe.create_tic_tac_toe,
left_div,
source=inspect.getsource(tictactoe),
)
left_div.append(shoelace.Divider(style={"margin-top": "5px", "margin-bottom": "30px"}))
left_div.append(el.a("Examples", href="/test/ui/", style={"text-align": "left"}))
# ********** CREATE ALL THE LAYOUT **********
grid = el.grid("minmax(100px, 200px) 20px auto", style={"min-height": "100%"})
grid.append(left_div)
grid.append(shoelace.Divider(vertical=True))
grid.append(main_area)
pydom.body.append(grid)
pydom.body.append(el.a("Back to the main page", href="/test/ui/", target="_blank"))
pydom.body.append(el.a("Hidden!!!", href="/test/ui/", target="_blank", hidden=True))

View File

@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>PyDom UI</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="../../dist/core.css">
<script type="module" src="../../dist/core.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked@11.1.1/lib/marked.umd.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<!-- and it's easy to individually load additional languages -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/go.min.js"></script>
<!-- SHOWLACE CUSTOM CSS -->
<style>
</style>
<style>
body {
font-family: -apple-system, "system-ui", "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"
}
input:invalid {
background-color: lightpink;
}
</style>
</head>
<body>
<script type="mpy" src="./demo.py" config="./pyscript.toml"></script>
</body>
</html>

View File

@@ -0,0 +1,8 @@
packages = []
[files]
"./examples.py" = "./examples.py"
"./tictactoe.py" = "./tictactoe.py"
"./styles.py" = "./styles.py"
"./shoelace.py" = "./shoelace.py"
"./markdown.py" = "./markdown.py"

View File

@@ -0,0 +1,2 @@
[files]
"./test.py" = "./test.py"

View File

@@ -0,0 +1,30 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<script type="module" src="../../dist/core.js"></script>
</head>
<body>
<script type="mpy" async>
from pyscript import create_named_worker
await create_named_worker("./worker.py", name="micropython_version", type="mpy")
</script>
<script type="mpy" config="./config.toml" async>
from test import test
await test("mpy")
</script>
<script type="py" config="./config.toml" async>
from test import test
await test("py")
</script>
<script type="py" name="pyodide_version" worker>
def pyodide_version():
import sys
return sys.version
__export__ = ['pyodide_version']
</script>
</body>
</html>

View File

@@ -0,0 +1,29 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>named workers</title>
<script type="module" src="../../dist/core.js"></script>
</head>
<body>
<script type="mpy" async>
from pyscript import workers
await (await workers["mpy"]).greetings()
await (await workers["py"]).greetings()
</script>
<script type="mpy" worker name="mpy">
def greetings():
print("micropython")
__export__ = ['greetings']
</script>
<script type="py" worker name="py">
def greetings():
print("pyodide")
__export__ = ['greetings']
</script>
</body>
</html>

View File

@@ -0,0 +1,19 @@
from pyscript import document, workers
async def test(interpreter):
# accessed as item
named = await workers.micropython_version
version = await named.micropython_version()
document.body.append(version)
document.body.append(document.createElement("hr"))
# accessed as attribute
named = await workers["pyodide_version"]
version = await named.pyodide_version()
document.body.append(version)
document.body.append(document.createElement("hr"))
document.documentElement.classList.add(interpreter)

View File

@@ -0,0 +1,7 @@
def micropython_version():
import sys
return sys.version
__export__ = ["micropython_version"]

View File

@@ -0,0 +1,6 @@
import { test, expect } from '@playwright/test';
test('MicroPython WebSocket', async ({ page }) => {
await page.goto('http://localhost:5037/');
await page.waitForSelector('html.ok');
});

View File

@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="../../dist/core.css">
<script type="module" src="../../dist/core.js"></script>
</head>
<body>
<script type="mpy" worker>
from pyscript import WebSocket, document
def onopen(event):
print(event.type)
ws.send("hello")
def onmessage(event):
print(event.type, event.data)
ws.close()
def onclose(event):
print(event.type)
document.documentElement.classList.add("ok")
ws = WebSocket(
url="ws://localhost:5037/",
onopen=onopen,
onmessage=onmessage,
onclose=onclose
)
</script>
</body>
</html>

View File

@@ -0,0 +1,33 @@
import { serve, file } from 'bun';
import path, { dirname, join } from 'path';
import { fileURLToPath } from 'url';
const dir = dirname(fileURLToPath(import.meta.url));
serve({
port: 5037,
fetch(req, server) {
if (server.upgrade(req)) return;
const url = new URL(req.url);
let { pathname } = url;
if (pathname === '/') pathname = '/index.html';
else if (/^\/dist\//.test(pathname)) pathname = `/../..${pathname}`;
else if (pathname === '/favicon.ico')
return new Response('Not Found', { status: 404 });
const response = new Response(file(`${dir}${pathname}`));
const { headers } = response;
headers.set('Cross-Origin-Opener-Policy', 'same-origin');
headers.set('Cross-Origin-Embedder-Policy', 'require-corp');
headers.set('Cross-Origin-Resource-Policy', 'cross-origin');
return response;
},
websocket: {
message(ws, message) {
ws.send(message);
},
close() {
process.exit(0);
}
},
});

View File

@@ -186,12 +186,12 @@ class TestSupport(PyScriptTest):
#
msg = str(exc.value)
expected = textwrap.dedent(
"""
f"""
JS errors found: 2
Error: error 1
at https://fake_server/mytest.html:.*
at {self.http_server_addr}/mytest.html:.*
Error: error 2
at https://fake_server/mytest.html:.*
at {self.http_server_addr}/mytest.html:.*
"""
).strip()
assert re.search(expected, msg)
@@ -217,12 +217,12 @@ class TestSupport(PyScriptTest):
#
msg = str(exc.value)
expected = textwrap.dedent(
"""
f"""
JS errors found: 2
Error: NOT expected 2
at https://fake_server/mytest.html:.*
at {self.http_server_addr}/mytest.html:.*
Error: NOT expected 4
at https://fake_server/mytest.html:.*
at {self.http_server_addr}/mytest.html:.*
"""
).strip()
assert re.search(expected, msg)
@@ -243,15 +243,15 @@ class TestSupport(PyScriptTest):
#
msg = str(exc.value)
expected = textwrap.dedent(
"""
f"""
The following JS errors were expected but could not be found:
- this is not going to be found
---
The following JS errors were raised but not expected:
Error: error 1
at https://fake_server/mytest.html:.*
at {self.http_server_addr}/mytest.html:.*
Error: error 2
at https://fake_server/mytest.html:.*
at {self.http_server_addr}/mytest.html:.*
"""
).strip()
assert re.search(expected, msg)
@@ -471,6 +471,8 @@ class TestSupport(PyScriptTest):
Test that we capture a 404 in loading a page that does not exist.
"""
self.goto("this_url_does_not_exist.html")
assert [
"Failed to load resource: the server responded with a status of 404 (Not Found)"
] == self.console.all.lines
if self.dev_server:
error = "Failed to load resource: the server responded with a status of 404 (File not found)"
else:
error = "Failed to load resource: the server responded with a status of 404 (Not Found)"
assert [error] == self.console.all.lines

View File

@@ -97,7 +97,7 @@ class TestBasic(PyScriptTest):
def test_input_exception(self):
self.pyscript_run(
"""
<script type="py">
<script type="py" async="false">
input("what's your name?")
</script>
"""

View File

@@ -43,12 +43,12 @@ class TestDisplay(PyScriptTest):
def test_consecutive_display(self):
self.pyscript_run(
"""
<script type="py">
<script type="py" async="false">
from pyscript import display
display('hello 1')
</script>
<p>hello 2</p>
<script type="py">
<script type="py" async="false">
from pyscript import display
display('hello 3')
</script>
@@ -177,16 +177,16 @@ class TestDisplay(PyScriptTest):
def test_consecutive_display_target(self):
self.pyscript_run(
"""
<script type="py" id="first">
<script type="py" id="first" async="false">
from pyscript import display
display('hello 1')
</script>
<p>hello in between 1 and 2</p>
<script type="py" id="second">
<script type="py" id="second" async="false">
from pyscript import display
display('hello 2', target="second")
</script>
<script type="py" id="third">
<script type="py" id="third" async="false">
from pyscript import display
display('hello 3')
</script>

View File

@@ -0,0 +1,804 @@
import re
import pytest
from .support import PyScriptTest, only_main, skip_worker
DEFAULT_ELEMENT_ATTRIBUTES = {
"accesskey": "s",
"autocapitalize": "off",
"autofocus": True,
"contenteditable": True,
"draggable": True,
"enterkeyhint": "go",
"hidden": False,
"id": "whateverid",
"lang": "br",
"nonce": "123",
"part": "part1:exposed1",
"popover": True,
"slot": "slot1",
"spellcheck": False,
"tabindex": 3,
"title": "whatevertitle",
"translate": "no",
"virtualkeyboardpolicy": "manual",
}
INTERPRETERS = ["py", "mpy"]
@pytest.fixture(params=INTERPRETERS)
def interpreter(request):
return request.param
class TestElements(PyScriptTest):
"""Test all elements in the pyweb.ui.elements module.
This class tests all elements in the pyweb.ui.elements module. It creates
an element of each type, both executing in the main thread and in a worker.
It runs each test for each interpreter defined in `INTERPRETERS`
Each individual element test looks for the element properties, sets a value
on each the supported properties and checks if the element was created correctly
and all it's properties were set correctly.
"""
@property
def expected_missing_file_errors(self):
# In fake server conditions this test will not throw an error due to missing files.
# If we want to skip the test, use:
# pytest.skip("Skipping: fake server doesn't throw 404 errors on missing local files.")
return (
[
"Failed to load resource: the server responded with a status of 404 (File not found)"
]
if self.dev_server
else []
)
def _create_el_and_basic_asserts(
self,
el_type,
el_text=None,
interpreter="py",
properties=None,
expected_errors=None,
additional_selector_rules=None,
):
"""Create an element with all its properties set, by running <script type=<interpreter> ... >
, and check if the element was created correctly and all its properties were set correctly.
"""
expected_errors = expected_errors or []
if not properties:
properties = {}
def parse_value(v):
if isinstance(v, bool):
return str(v)
return f"'{v}'"
attributes = ""
if el_text:
attributes += f'"{el_text}",'
if properties:
attributes += ", ".join(
[f"{k}={parse_value(v)}" for k, v in properties.items()]
)
# Let's make sure the body of the page is clean first
body = self.page.locator("body")
assert body.inner_html() == ""
# Let's make sure the element is not in the page
element = self.page.locator(el_type)
assert not element.count()
# Let's create the element
code_ = f"""
from pyscript import when
<script type="{interpreter}">
from pyscript.web import page, {el_type}
el = {el_type}({attributes})
page.body.append(el)
</script>
"""
self.pyscript_run(code_)
# Let's keep the tag in 2 variables, one for the selector and another to
# check the return tag from the selector
locator_type = el_tag = el_type[:-1] if el_type.endswith("_") else el_type
if additional_selector_rules:
locator_type += f"{additional_selector_rules}"
el = self.page.locator(locator_type)
tag = el.evaluate("node => node.tagName")
assert tag == el_tag.upper()
if el_text:
assert el.inner_html() == el_text
assert el.text_content() == el_text
# if we expect specific errors, check that they are in the console
if expected_errors:
for error in expected_errors:
assert error in self.console.error.lines
else:
# if we don't expect errors, check that there are no errors
assert self.console.error.lines == []
if properties:
for k, v in properties.items():
actual_val = el.evaluate(f"node => node.{k}")
assert actual_val == v
return el
def test_a(self, interpreter):
a = self._create_el_and_basic_asserts("a", "click me", interpreter)
assert a.text_content() == "click me"
def test_abbr(self, interpreter):
abbr = self._create_el_and_basic_asserts(
"abbr", "some text", interpreter=interpreter
)
assert abbr.text_content() == "some text"
def test_address(self, interpreter):
address = self._create_el_and_basic_asserts("address", "some text", interpreter)
assert address.text_content() == "some text"
def test_area(self, interpreter):
properties = {
"shape": "poly",
"coords": "129,0,260,95,129,138",
"href": "https://developer.mozilla.org/docs/Web/HTTP",
"target": "_blank",
"alt": "HTTP",
}
# TODO: Check why click times out
self._create_el_and_basic_asserts(
"area", interpreter=interpreter, properties=properties
)
def test_article(self, interpreter):
self._create_el_and_basic_asserts("article", "some text", interpreter)
def test_aside(self, interpreter):
self._create_el_and_basic_asserts("aside", "some text", interpreter)
def test_audio(self, interpreter):
self._create_el_and_basic_asserts(
"audio",
interpreter=interpreter,
properties={"src": "http://localhost:8080/somefile.ogg", "controls": True},
expected_errors=self.expected_missing_file_errors,
)
def test_b(self, interpreter):
self._create_el_and_basic_asserts("b", "some text", interpreter)
def test_blockquote(self, interpreter):
self._create_el_and_basic_asserts("blockquote", "some text", interpreter)
def test_br(self, interpreter):
self._create_el_and_basic_asserts("br", interpreter=interpreter)
def test_element_button(self, interpreter):
button = self._create_el_and_basic_asserts("button", "click me", interpreter)
assert button.inner_html() == "click me"
def test_element_button_attributes(self, interpreter):
button = self._create_el_and_basic_asserts(
"button", "click me", interpreter, None
)
assert button.inner_html() == "click me"
def test_canvas(self, interpreter):
properties = {
"height": 100,
"width": 120,
}
# TODO: Check why click times out
self._create_el_and_basic_asserts(
"canvas", "alt text for canvas", interpreter, properties=properties
)
def test_caption(self, interpreter):
self._create_el_and_basic_asserts("caption", "some text", interpreter)
def test_cite(self, interpreter):
self._create_el_and_basic_asserts("cite", "some text", interpreter)
def test_code(self, interpreter):
self._create_el_and_basic_asserts("code", "import pyweb", interpreter)
def test_data(self, interpreter):
self._create_el_and_basic_asserts(
"data", "some text", interpreter, properties={"value": "123"}
)
def test_datalist(self, interpreter):
self._create_el_and_basic_asserts("datalist", "some items", interpreter)
def test_dd(self, interpreter):
self._create_el_and_basic_asserts("dd", "some text", interpreter)
def test_del_(self, interpreter):
self._create_el_and_basic_asserts(
"del_", "some text", interpreter, properties={"cite": "http://example.com/"}
)
def test_details(self, interpreter):
self._create_el_and_basic_asserts(
"details", "some text", interpreter, properties={"open": True}
)
def test_dialog(self, interpreter):
self._create_el_and_basic_asserts(
"dialog", "some text", interpreter, properties={"open": True}
)
def test_div(self, interpreter):
div = self._create_el_and_basic_asserts("div", "click me", interpreter)
assert div.inner_html() == "click me"
def test_dl(self, interpreter):
self._create_el_and_basic_asserts("dl", "some text", interpreter)
def test_dt(self, interpreter):
self._create_el_and_basic_asserts("dt", "some text", interpreter)
def test_em(self, interpreter):
self._create_el_and_basic_asserts("em", "some text", interpreter)
def test_embed(self, interpreter):
# NOTE: Types actually matter and embed expects a string for height and width
# while other elements expect an int
# TODO: It's important that we add typing soon to help with the user experience
properties = {
"src": "http://localhost:8080/somefile.ogg",
"type": "video/ogg",
"width": "250",
"height": "200",
}
self._create_el_and_basic_asserts(
"embed",
interpreter=interpreter,
properties=properties,
expected_errors=self.expected_missing_file_errors,
)
def test_fieldset(self, interpreter):
self._create_el_and_basic_asserts(
"fieldset", "some text", interpreter, properties={"name": "some name"}
)
def test_figcaption(self, interpreter):
self._create_el_and_basic_asserts("figcaption", "some text", interpreter)
def test_figure(self, interpreter):
self._create_el_and_basic_asserts("figure", "some text", interpreter)
def test_footer(self, interpreter):
self._create_el_and_basic_asserts("footer", "some text", interpreter)
def test_form(self, interpreter):
properties = {
"action": "https://example.com/submit",
"method": "post",
"name": "some name",
"autocomplete": "on",
"rel": "external",
}
self._create_el_and_basic_asserts(
"form", "some text", interpreter, properties=properties
)
def test_h1(self, interpreter):
self._create_el_and_basic_asserts("h1", "some text", interpreter)
def test_h2(self, interpreter):
self._create_el_and_basic_asserts("h2", "some text", interpreter)
def test_h3(self, interpreter):
self._create_el_and_basic_asserts("h3", "some text", interpreter)
def test_h4(self, interpreter):
self._create_el_and_basic_asserts("h4", "some text", interpreter)
def test_h5(self, interpreter):
self._create_el_and_basic_asserts("h5", "some text", interpreter)
def test_h6(self, interpreter):
self._create_el_and_basic_asserts("h6", "some text", interpreter)
def test_header(self, interpreter):
self._create_el_and_basic_asserts("header", "some text", interpreter)
def test_hgroup(self, interpreter):
self._create_el_and_basic_asserts("hgroup", "some text", interpreter)
def test_hr(self, interpreter):
self._create_el_and_basic_asserts("hr", interpreter=interpreter)
def test_i(self, interpreter):
self._create_el_and_basic_asserts("i", "some text", interpreter)
def test_iframe(self, interpreter):
# TODO: same comment about defining the right types
properties = {
"src": "http://localhost:8080/somefile.html",
"width": "250",
"height": "200",
}
self._create_el_and_basic_asserts(
"iframe",
interpreter,
properties=properties,
expected_errors=self.expected_missing_file_errors,
)
def test_img(self, interpreter):
properties = {
"src": "http://localhost:8080/somefile.png",
"alt": "some image",
"width": 250,
"height": 200,
}
self._create_el_and_basic_asserts(
"img",
interpreter=interpreter,
properties=properties,
expected_errors=self.expected_missing_file_errors,
)
def test_input(self, interpreter):
# TODO: we need multiple input tests
properties = {
"type": "text",
"value": "some value",
"name": "some name",
"autofocus": True,
"pattern": "[A-Za-z]{3}",
"placeholder": "some placeholder",
"required": True,
"size": 20,
}
self._create_el_and_basic_asserts(
"input_", interpreter=interpreter, properties=properties
)
def test_ins(self, interpreter):
self._create_el_and_basic_asserts(
"ins", "some text", interpreter, properties={"cite": "http://example.com/"}
)
def test_kbd(self, interpreter):
self._create_el_and_basic_asserts("kbd", "some text", interpreter)
def test_label(self, interpreter):
self._create_el_and_basic_asserts("label", "some text", interpreter)
def test_legend(self, interpreter):
self._create_el_and_basic_asserts("legend", "some text", interpreter)
def test_li(self, interpreter):
self._create_el_and_basic_asserts("li", "some text", interpreter)
def test_link(self, interpreter):
properties = {
"href": "http://localhost:8080/somefile.css",
"rel": "stylesheet",
"type": "text/css",
}
self._create_el_and_basic_asserts(
"link",
interpreter=interpreter,
properties=properties,
expected_errors=self.expected_missing_file_errors,
additional_selector_rules="[href='http://localhost:8080/somefile.css']",
)
def test_main(self, interpreter):
self._create_el_and_basic_asserts("main", "some text", interpreter)
def test_map(self, interpreter):
self._create_el_and_basic_asserts(
"map_", "some text", interpreter, properties={"name": "somemap"}
)
def test_mark(self, interpreter):
self._create_el_and_basic_asserts("mark", "some text", interpreter)
def test_menu(self, interpreter):
self._create_el_and_basic_asserts("menu", "some text", interpreter)
def test_meter(self, interpreter):
properties = {
"value": 50,
"min": 0,
"max": 100,
"low": 30,
"high": 80,
"optimum": 50,
}
self._create_el_and_basic_asserts(
"meter", "some text", interpreter, properties=properties
)
def test_nav(self, interpreter):
self._create_el_and_basic_asserts("nav", "some text", interpreter)
def test_object(self, interpreter):
properties = {
"data": "http://localhost:8080/somefile.swf",
"type": "application/x-shockwave-flash",
"width": "250",
"height": "200",
}
self._create_el_and_basic_asserts(
"object_",
interpreter=interpreter,
properties=properties,
)
def test_ol(self, interpreter):
self._create_el_and_basic_asserts("ol", "some text", interpreter)
def test_optgroup(self, interpreter):
self._create_el_and_basic_asserts(
"optgroup", "some text", interpreter, properties={"label": "some label"}
)
def test_option(self, interpreter):
self._create_el_and_basic_asserts(
"option", "some text", interpreter, properties={"value": "some value"}
)
def test_output(self, interpreter):
self._create_el_and_basic_asserts("output", "some text", interpreter)
def test_p(self, interpreter):
self._create_el_and_basic_asserts("p", "some text", interpreter)
def test_picture(self, interpreter):
self._create_el_and_basic_asserts("picture", "some text", interpreter)
def test_pre(self, interpreter):
self._create_el_and_basic_asserts("pre", "some text", interpreter)
def test_progress(self, interpreter):
properties = {
"value": 50,
"max": 100,
}
self._create_el_and_basic_asserts(
"progress", "some text", interpreter, properties=properties
)
def test_q(self, interpreter):
self._create_el_and_basic_asserts(
"q", "some text", interpreter, properties={"cite": "http://example.com/"}
)
def test_s(self, interpreter):
self._create_el_and_basic_asserts("s", "some text", interpreter)
# def test_script(self):
# self._create_el_and_basic_asserts("script", "some text")
def test_section(self, interpreter):
self._create_el_and_basic_asserts("section", "some text", interpreter)
def test_select(self, interpreter):
self._create_el_and_basic_asserts("select", "some text", interpreter)
def test_small(self, interpreter):
self._create_el_and_basic_asserts("small", "some text", interpreter)
def test_source(self, interpreter):
properties = {
"src": "http://localhost:8080/somefile.ogg",
"type": "audio/ogg",
}
self._create_el_and_basic_asserts(
"source",
interpreter=interpreter,
properties=properties,
)
def test_span(self, interpreter):
self._create_el_and_basic_asserts("span", "some text", interpreter)
def test_strong(self, interpreter):
self._create_el_and_basic_asserts("strong", "some text", interpreter)
def test_style(self, interpreter):
self._create_el_and_basic_asserts(
"style",
"body {background-color: red;}",
interpreter,
)
def test_sub(self, interpreter):
self._create_el_and_basic_asserts("sub", "some text", interpreter)
def test_summary(self, interpreter):
self._create_el_and_basic_asserts("summary", "some text", interpreter)
def test_sup(self, interpreter):
self._create_el_and_basic_asserts("sup", "some text", interpreter)
def test_table(self, interpreter):
self._create_el_and_basic_asserts("table", "some text", interpreter)
def test_tbody(self, interpreter):
self._create_el_and_basic_asserts("tbody", "some text", interpreter)
def test_td(self, interpreter):
self._create_el_and_basic_asserts("td", "some text", interpreter)
def test_template(self, interpreter):
# We are not checking the content of template since it's sort of
# special element
self._create_el_and_basic_asserts("template", interpreter=interpreter)
def test_textarea(self, interpreter):
self._create_el_and_basic_asserts("textarea", "some text", interpreter)
def test_tfoot(self, interpreter):
self._create_el_and_basic_asserts("tfoot", "some text", interpreter)
def test_th(self, interpreter):
self._create_el_and_basic_asserts("th", "some text", interpreter)
def test_thead(self, interpreter):
self._create_el_and_basic_asserts("thead", "some text", interpreter)
def test_time(self, interpreter):
self._create_el_and_basic_asserts("time", "some text", interpreter)
def test_title(self, interpreter):
self._create_el_and_basic_asserts("title", "some text", interpreter)
def test_tr(self, interpreter):
self._create_el_and_basic_asserts("tr", "some text", interpreter)
def test_track(self, interpreter):
properties = {
"src": "http://localhost:8080/somefile.vtt",
"kind": "subtitles",
"srclang": "en",
"label": "English",
}
self._create_el_and_basic_asserts(
"track",
interpreter=interpreter,
properties=properties,
)
def test_u(self, interpreter):
self._create_el_and_basic_asserts("u", "some text", interpreter)
def test_ul(self, interpreter):
self._create_el_and_basic_asserts("ul", "some text", interpreter)
def test_var(self, interpreter):
self._create_el_and_basic_asserts("var", "some text", interpreter)
def test_video(self, interpreter):
properties = {
"src": "http://localhost:8080/somefile.ogg",
"controls": True,
"width": 250,
"height": 200,
}
self._create_el_and_basic_asserts(
"video",
interpreter=interpreter,
properties=properties,
expected_errors=self.expected_missing_file_errors,
)
def test_append_py_element(self, interpreter):
# Let's make sure the body of the page is clean first
body = self.page.locator("body")
assert body.inner_html() == ""
# Let's make sure the element is not in the page
element = self.page.locator("div")
assert not element.count()
div_text_content = "Luke, I am your father"
p_text_content = "noooooooooo!"
# Let's create the element
code_ = f"""
from pyscript import when
<script type="{interpreter}">
from pyscript.web import page, div, p
el = div("{div_text_content}")
child = p('{p_text_content}')
el.append(child)
page.body.append(el)
</script>
"""
self.pyscript_run(code_)
# Let's keep the tag in 2 variables, one for the selector and another to
# check the return tag from the selector
el = self.page.locator("div")
tag = el.evaluate("node => node.tagName")
assert tag == "DIV"
assert el.text_content() == f"{div_text_content}{p_text_content}"
assert (
el.evaluate("node => node.children.length") == 1
), "There should be only 1 child"
assert el.evaluate("node => node.children[0].tagName") == "P"
assert (
el.evaluate("node => node.children[0].parentNode.textContent")
== f"{div_text_content}{p_text_content}"
)
assert el.evaluate("node => node.children[0].textContent") == p_text_content
def test_append_proxy_element(self, interpreter):
# Let's make sure the body of the page is clean first
body = self.page.locator("body")
assert body.inner_html() == ""
# Let's make sure the element is not in the page
element = self.page.locator("div")
assert not element.count()
div_text_content = "Luke, I am your father"
p_text_content = "noooooooooo!"
# Let's create the element
code_ = f"""
from pyscript import when
<script type="{interpreter}">
from pyscript import document
from pyscript.web import page, div, p
el = div("{div_text_content}")
child = document.createElement('P')
child.textContent = '{p_text_content}'
el.append(child)
page.body.append(el)
</script>
"""
self.pyscript_run(code_)
# Let's keep the tag in 2 variables, one for the selector and another to
# check the return tag from the selector
el = self.page.locator("div")
tag = el.evaluate("node => node.tagName")
assert tag == "DIV"
assert el.text_content() == f"{div_text_content}{p_text_content}"
assert (
el.evaluate("node => node.children.length") == 1
), "There should be only 1 child"
assert el.evaluate("node => node.children[0].tagName") == "P"
assert (
el.evaluate("node => node.children[0].parentNode.textContent")
== f"{div_text_content}{p_text_content}"
)
assert el.evaluate("node => node.children[0].textContent") == p_text_content
def test_append_py_elementcollection(self, interpreter):
# Let's make sure the body of the page is clean first
body = self.page.locator("body")
assert body.inner_html() == ""
# Let's make sure the element is not in the page
element = self.page.locator("div")
assert not element.count()
div_text_content = "Luke, I am your father"
p_text_content = "noooooooooo!"
p2_text_content = "not me!"
# Let's create the element
code_ = f"""
from pyscript import when
<script type="{interpreter}">
from pyscript.web import page, div, p, ElementCollection
el = div("{div_text_content}")
child1 = p('{p_text_content}')
child2 = p('{p2_text_content}', id='child2')
collection = ElementCollection([child1, child2])
el.append(collection)
page.body.append(el)
</script>
"""
self.pyscript_run(code_)
# Let's keep the tag in 2 variables, one for the selector and another to
# check the return tag from the selector
el = self.page.locator("div")
tag = el.evaluate("node => node.tagName")
assert tag == "DIV"
parent_full_content = f"{div_text_content}{p_text_content}{p2_text_content}"
assert el.text_content() == parent_full_content
assert (
el.evaluate("node => node.children.length") == 2
), "There should be only 1 child"
assert el.evaluate("node => node.children[0].tagName") == "P"
assert (
el.evaluate("node => node.children[0].parentNode.textContent")
== parent_full_content
)
assert el.evaluate("node => node.children[0].textContent") == p_text_content
assert el.evaluate("node => node.children[1].tagName") == "P"
assert el.evaluate("node => node.children[1].id") == "child2"
assert (
el.evaluate("node => node.children[1].parentNode.textContent")
== parent_full_content
)
assert el.evaluate("node => node.children[1].textContent") == p2_text_content
def test_append_js_element_nodelist(self, interpreter):
# Let's make sure the body of the page is clean first
body = self.page.locator("body")
assert body.inner_html() == ""
# Let's make sure the element is not in the page
element = self.page.locator("div")
assert not element.count()
div_text_content = "Luke, I am your father"
p_text_content = "noooooooooo!"
p2_text_content = "not me!"
# Let's create the element
code_ = f"""
from pyscript import when
<script type="{interpreter}">
from pyscript import document
from pyscript.web import page, div, p, ElementCollection
el = div("{div_text_content}")
child1 = p('{p_text_content}')
child2 = p('{p2_text_content}', id='child2')
page.body.append(child1)
page.body.append(child2)
nodes = document.querySelectorAll('p')
el.append(nodes)
page.body.append(el)
</script>
"""
self.pyscript_run(code_)
# Let's keep the tag in 2 variables, one for the selector and another to
# check the return tag from the selector
el = self.page.locator("div")
tag = el.evaluate("node => node.tagName")
assert tag == "DIV"
parent_full_content = f"{div_text_content}{p_text_content}{p2_text_content}"
assert el.text_content() == parent_full_content
assert (
el.evaluate("node => node.children.length") == 2
), "There should be only 1 child"
assert el.evaluate("node => node.children[0].tagName") == "P"
assert (
el.evaluate("node => node.children[0].parentNode.textContent")
== parent_full_content
)
assert el.evaluate("node => node.children[0].textContent") == p_text_content
assert el.evaluate("node => node.children[1].tagName") == "P"
assert el.evaluate("node => node.children[1].id") == "child2"
assert (
el.evaluate("node => node.children[1].parentNode.textContent")
== parent_full_content
)
assert el.evaluate("node => node.children[1].textContent") == p2_text_content

View File

@@ -0,0 +1,4 @@
declare var r: any;
declare var n: any;
declare var t: {};
export { r as WebLinksAddon, n as __esModule, t as default };

View File

@@ -1,2 +1,2 @@
export default configs;
declare const configs: Map<any, any>;
export const configs: Map<any, any>;
export function relative_url(url: any, base?: string): string;

View File

@@ -1,4 +1,9 @@
export function offline_interpreter(config: any): string;
import { stdlib } from "./stdlib.js";
import { optional } from "./stdlib.js";
import { inputFailure } from "./hooks.js";
import TYPES from "./types.js";
import { relative_url } from "./config.js";
/**
* A `Worker` facade able to bootstrap on the worker thread only a PyScript module.
* @param {string} file the python file to run ina worker.
@@ -38,11 +43,7 @@ declare const exportedHooks: {
};
worker: {
onReady: Set<Function>;
onBeforeRun: Set<Function>; /**
* Given a generic DOM Element, tries to fetch the 'src' attribute, if present.
* It either throws an error if the 'src' can't be fetched or it returns a fallback
* content as source.
*/
onBeforeRun: Set<Function>;
onBeforeRunAsync: Set<Function>;
onAfterRun: Set<Function>;
onAfterRunAsync: Set<Function>;
@@ -53,6 +54,5 @@ declare const exportedHooks: {
};
};
declare const exportedConfig: {};
declare const exportedWhenDefined: (type: string) => Promise<any>;
import sync from "./sync.js";
export { TYPES, exportedPyWorker as PyWorker, exportedMPWorker as MPWorker, exportedHooks as hooks, exportedConfig as config, exportedWhenDefined as whenDefined };
declare const exportedWhenDefined: (type: string) => Promise<object>;
export { stdlib, optional, inputFailure, TYPES, relative_url, exportedPyWorker as PyWorker, exportedMPWorker as MPWorker, exportedHooks as hooks, exportedConfig as config, exportedWhenDefined as whenDefined };

View File

@@ -53,19 +53,4 @@ export class InstallError extends UserError {
/**
* Keys of the ErrorCode object
*/
export type ErrorCodes = keyof {
GENERIC: string;
CONFLICTING_CODE: string;
BAD_CONFIG: string;
MICROPIP_INSTALL_ERROR: string;
BAD_PLUGIN_FILE_EXTENSION: string;
NO_DEFAULT_EXPORT: string;
TOP_LEVEL_AWAIT: string;
FETCH_ERROR: string;
FETCH_NAME_ERROR: string;
FETCH_UNAUTHORIZED_ERROR: string;
FETCH_FORBIDDEN_ERROR: string;
FETCH_NOT_FOUND_ERROR: string;
FETCH_SERVER_ERROR: string;
FETCH_UNAVAILABLE_ERROR: string;
};
export type ErrorCodes = "GENERIC" | "CONFLICTING_CODE" | "BAD_CONFIG" | "MICROPIP_INSTALL_ERROR" | "BAD_PLUGIN_FILE_EXTENSION" | "NO_DEFAULT_EXPORT" | "TOP_LEVEL_AWAIT" | "FETCH_ERROR" | "FETCH_NAME_ERROR" | "FETCH_UNAUTHORIZED_ERROR" | "FETCH_FORBIDDEN_ERROR" | "FETCH_NOT_FOUND_ERROR" | "FETCH_SERVER_ERROR" | "FETCH_UNAVAILABLE_ERROR";

View File

@@ -1,7 +1,8 @@
export function main(name: any): any;
export function worker(name: any): any;
export function codeFor(branch: any): {};
export function codeFor(branch: any, type: any): {};
export function createFunction(self: any, name: any): any;
export const inputFailure: "\n import builtins\n def input(prompt=\"\"):\n raise Exception(\"\\n \".join([\n \"input() doesn't work when PyScript runs in the main thread.\",\n \"Consider using the worker attribute: https://pyscript.github.io/docs/2023.11.2/user-guide/workers/\"\n ]))\n\n builtins.input = input\n del builtins\n del input\n";
export namespace hooks {
namespace main {
let onWorker: Set<Function>;

View File

@@ -1,2 +1 @@
declare const _default: Promise<void>;
export default _default;
export {};

View File

@@ -0,0 +1,2 @@
declare function _default(element: any): Promise<void>;
export default _default;

View File

@@ -0,0 +1,2 @@
declare function _default(element: any): Promise<void>;
export default _default;

Some files were not shown because too many files have changed in this diff Show More