Compare commits

...

55 Commits

Author SHA1 Message Date
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
Andrea Giammarchi
a1268f1aa2 Fix #1993 - Expose a handy fetch API (#1994) 2024-03-14 19:36:23 +01:00
Christian Clauss
69b8884045 Keep GitHub Actions up to date with GitHub's Dependabot (#1992)
* Keep GitHub Actions up to date with GitHub's Dependabot

Fix the warning like at the bottom right of
https://github.com/pyscript/pyscript/actions/runs/8263920642

# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem

* [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-14 15:36:18 +01:00
Andrea Giammarchi
df1d699fe6 [feature] py-editor setup (#1989) 2024-03-13 12:25:30 +01:00
Andrea Giammarchi
84f197b657 Updated polyscript to its latest (#1982) 2024-02-15 17:49:09 +01:00
Andrea Giammarchi
5bed5ede52 Fix #1974 - Use utf-8 encoding to bootstrap stdlib (#1981) 2024-02-14 09:59:18 +01:00
Fabio Pliger
f6d5cf06c8 Add text to pydom.Element (#1911)
* add missing test for html attribute

* add test for text attribute

* fix text attribute 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>
Co-authored-by: Andrea Giammarchi <andrea.giammarchi@gmail.com>
2024-02-12 11:50:36 -08:00
Andrea Giammarchi
30c6c830ae Fix #1972 - Evaluate users' code a part (#1975) 2024-02-09 15:52:29 +01:00
Shubhal Gupta
d7084f7f55 Fix: Restored the development docs #1783 (#1803)
* Fix: Restored the development docs #1783

* [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: Andrea Giammarchi <andrea.giammarchi@gmail.com>
2024-02-07 11:17:50 +01:00
pre-commit-ci[bot]
a87d2b3fea [pre-commit.ci] pre-commit autoupdate (#1917)
updates:
- [github.com/psf/black: 23.11.0 → 24.1.1](https://github.com/psf/black/compare/23.11.0...24.1.1)
- [github.com/pycqa/isort: 5.12.0 → 5.13.2](https://github.com/pycqa/isort/compare/5.12.0...5.13.2)

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-02-07 11:13:05 +01:00
Fábio Rosado
81a26363a3 Update bug template to point users to pyscript.com issue tracker (#1971)
* Update bug template to point users to pyscript.com issue tracker

* add bold to make it more visible
2024-02-06 14:23:12 +00:00
Andrea Giammarchi
53e945201d Update polyscript to fix a try/catch issue (#1968) 2024-02-02 15:33:26 +01:00
Andrea Giammarchi
181d276c8b Allow Workers w/out SharedArrayBuffer (#1967)
Allow Workers w/out SharedArrayBuffer
2024-02-02 15:03:30 +01:00
81 changed files with 4963 additions and 890 deletions

View File

@@ -11,7 +11,9 @@ body:
There will always be more issues than there is time to do them, and so we will need to selectively close issues that don't provide enough information, so we can focus our time on helping people like you who fill out the issue form completely. Thank you for your collaboration! There will always be more issues than there is time to do them, and so we will need to selectively close issues that don't provide enough information, so we can focus our time on helping people like you who fill out the issue form completely. Thank you for your collaboration!
There are also already a lot of open issues, so please take 2 minutes and search through existing ones to see if what you are experiencing already exists There are also already a lot of open issues, so please take 2 minutes and search through existing ones to see if what you are experiencing already exists.
Finally, if you are opening **a bug report related to PyScript.com** please [use this repository instead](https://github.com/anaconda/pyscript-dot-com-issues/issues/new/choose).
Thanks for helping PyScript be amazing. We are nothing without people like you helping build a better community 💐! Thanks for helping PyScript be amazing. We are nothing without people like you helping build a better community 💐!
- type: checkboxes - type: checkboxes

13
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,13 @@
# Keep GitHub Actions up to date with GitHub's Dependabot...
# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem
version: 2
updates:
- package-ecosystem: github-actions
directory: /
groups:
github-actions:
patterns:
- "*" # Group all Actions updates into a single larger pull request
schedule:
interval: weekly

View File

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

View File

@@ -19,12 +19,12 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install node - name: Install node
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: 18.x node-version: 18.x
- name: Cache node modules - name: Cache node modules
uses: actions/cache@v3 uses: actions/cache@v4
env: env:
cache-name: cache-node-modules cache-name: cache-node-modules
with: with:
@@ -46,6 +46,10 @@ jobs:
working-directory: . working-directory: .
run: sed 's#_PATH_#https://pyscript.net/releases/${{ github.ref_name }}/#' ./public/index.html > ./pyscript.core/dist/index.html 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 - name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4 uses: aws-actions/configure-aws-credentials@v4
with: with:

View File

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

View File

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

View File

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

View File

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

1
.gitignore vendored
View File

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

View File

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

View File

@@ -1,5 +1,15 @@
# Release Notes # 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 ## 2023.05.01
### Features ### 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 ## 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 ## License terms for contributions
@@ -79,3 +79,103 @@ The Project abides by the Organization's [trademark policy](https://github.com/p
Part of MVG-0.1-beta. Part of MVG-0.1-beta.
Made with love by GitHub. Licensed under the [CC-BY 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/). Made with love by GitHub. Licensed under the [CC-BY 4.0 License](https://creativecommons.org/licenses/by-sa/4.0/).
# Quick guide to pytest
We make heavy usage of pytest. Here is a quick guide and collection of useful options:
- To run all tests in the current directory and subdirectories: pytest
- To run tests in a specific directory or file: pytest path/to/dir/test_foo.py
- -s: disables output capturing
- --pdb: in case of exception, enter a (Pdb) prompt so that you can inspect what went wrong.
- -v: verbose mode
- -x: stop the execution as soon as one test fails
- -k foo: run only the tests whose full name contains foo
- -k 'foo and bar'
- -k 'foo and not bar'
## Running integration tests under pytest
make test is useful to run all the tests, but during the development is useful to have more control on how tests are run. The following guide assumes that you are in the directory pyscriptjs/tests/integration/.
### To run all the integration tests, single or multi core
$ pytest -xv
...
test_00_support.py::TestSupport::test_basic[chromium] PASSED [ 0%]
test_00_support.py::TestSupport::test_console[chromium] PASSED [ 1%]
test_00_support.py::TestSupport::test_check_js_errors_simple[chromium] PASSED [ 2%]
test_00_support.py::TestSupport::test_check_js_errors_expected[chromium] PASSED [ 3%]
test_00_support.py::TestSupport::test_check_js_errors_expected_but_didnt_raise[chromium] PASSED [ 4%]
test_00_support.py::TestSupport::test_check_js_errors_multiple[chromium] PASSED [ 5%]
...
-x means "stop at the first failure". -v means "verbose", so that you can see all the test names one by one. We try to keep tests in a reasonable order, from most basic to most complex. This way, if you introduced some bug in very basic things, you will notice immediately.
If you have the pytest-xdist plugin installed, you can run all the integration tests on 4 cores in parallel:
$ pytest -n 4
### To run a single test, headless
$ pytest test_01_basic.py -k test_pyscript_hello -s
...
[ 0.00 page.goto ] pyscript_hello.html
[ 0.01 request ] 200 - fake_server - http://fake_server/pyscript_hello.html
...
[ 0.17 console.info ] [py-loader] Downloading pyodide-x.y.z...
[ 0.18 request ] 200 - CACHED - https://cdn.jsdelivr.net/pyodide/vx.y.z/full/pyodide.js
...
[ 3.59 console.info ] [pyscript/main] PyScript page fully initialized
[ 3.60 console.log ] hello pyscript
-k selects tests by pattern matching as described above. -s instructs pytest to show the output to the terminal instead of capturing it. In the output you can see various useful things, including network requests and JS console messages.
### To run a single test, headed
$ pytest test_01_basic.py -k test_pyscript_hello -s --headed
...
Same as above, but with --headed the browser is shown in a window, and you can interact with it. The browser uses a fake server, which means that HTTP requests are cached.
Unfortunately, in this mode source maps does not seem to work, and you cannot debug the original typescript source code. This seems to be a bug in playwright, for which we have a workaround:
$ pytest test_01_basic.py -k test_pyscript_hello -s --headed --no-fake-server
...
As the name implies, -no-fake-server disables the fake server: HTTP requests are not cached, but source-level debugging works.
Finally:
$ pytest test_01_basic.py -k test_pyscript_hello -s --dev
...
--dev implies --headed --no-fake-server. In addition, it also automatically open chrome dev tools.
### To run only main thread or worker tests
By default, we run each test twice: one with execution_thread = "main" and one with execution_thread = "worker". If you want to run only half of them, you can use -m:
$ pytest -m main # run only the tests in the main thread
$ pytest -m worker # ron only the tests in the web worker
## Fake server, HTTP cache
By default, our test machinery uses a playwright router which intercepts and cache HTTP requests, so that for example you don't have to download pyodide again and again. This also enables the possibility of running tests in parallel on multiple cores.
The cache is stored using the pytest-cache plugin, which means that it survives across sessions.
If you want to temporarily disable the cache, the easiest thing is to use --no-fake-server, which bypasses it completely.
If you want to clear the cache, you can use the special option --clear-http-cache:
NOTE: this works only if you are inside tests/integration, or if you explicitly specify tests/integration from the command line. This is due to how pytest decides to search for and load the various conftest.py.

View File

@@ -38,11 +38,11 @@ To try PyScript, import the appropriate pyscript files into the `<head>` tag of
<head> <head>
<link <link
rel="stylesheet" rel="stylesheet"
href="https://pyscript.net/releases/2023.11.2/core.css" href="https://pyscript.net/releases/2024.5.2/core.css"
/> />
<script <script
type="module" type="module"
src="https://pyscript.net/releases/2023.11.2/core.js" src="https://pyscript.net/releases/2024.5.2/core.js"
></script> ></script>
</head> </head>
<body> <body>
@@ -67,10 +67,29 @@ Check out the [official docs](https://docs.pyscript.net/) for more detailed docu
## How to Contribute ## 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 ## Governance
The [PyScript organization governance](https://github.com/pyscript/governance) is documented in a separate repository. 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 .eslintrc.cjs
eslint.config.mjs
.pytest_cache/ .pytest_cache/
node_modules/ node_modules/
rollup/ rollup/
test/ test/
tests/ tests/
test-results/
src/stdlib/_pyscript src/stdlib/_pyscript
src/stdlib/pyscript.py src/stdlib/pyscript.py
package-lock.json 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", "name": "@pyscript/core",
"version": "0.3.23", "version": "0.4.42",
"type": "module", "type": "module",
"description": "PyScript", "description": "PyScript",
"module": "./index.js", "module": "./index.js",
@@ -20,13 +20,14 @@
}, },
"scripts": { "scripts": {
"server": "npx static-handler --coi .", "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 && eslint src/ && npm run ts && npm run test:mpy",
"build:core": "rm -rf dist && rollup --config rollup/core.config.js && cp src/3rd-party/*.css dist/", "build:core": "rm -rf dist && rollup --config rollup/core.config.js && cp src/3rd-party/*.css dist/",
"build:plugins": "node rollup/plugins.cjs", "build:plugins": "node rollup/plugins.cjs",
"build:stdlib": "node rollup/stdlib.cjs", "build:stdlib": "node rollup/stdlib.cjs",
"build:3rd-party": "node rollup/3rd-party.cjs", "build:3rd-party": "node rollup/3rd-party.cjs",
"clean:3rd-party": "rm src/3rd-party/*.js && rm src/3rd-party/*.css", "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", "dev": "node dev.cjs",
"release": "npm run build && npm run zip", "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", "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 +43,33 @@
"dependencies": { "dependencies": {
"@ungap/with-resolvers": "^0.1.0", "@ungap/with-resolvers": "^0.1.0",
"basic-devtools": "^0.1.6", "basic-devtools": "^0.1.6",
"polyscript": "^0.6.18", "polyscript": "^0.12.14",
"sticky-module": "^0.1.1", "sticky-module": "^0.1.1",
"to-json-callback": "^0.1.1", "to-json-callback": "^0.1.1",
"type-checked-collections": "^0.1.7" "type-checked-collections": "^0.1.7"
}, },
"devDependencies": { "devDependencies": {
"@codemirror/commands": "^6.3.3", "@codemirror/commands": "^6.6.0",
"@codemirror/lang-python": "^6.1.3", "@codemirror/lang-python": "^6.1.6",
"@codemirror/language": "^6.10.0", "@codemirror/language": "^6.10.2",
"@codemirror/state": "^6.4.0", "@codemirror/state": "^6.4.1",
"@codemirror/view": "^6.23.1", "@codemirror/view": "^6.27.0",
"@playwright/test": "^1.41.1", "@playwright/test": "^1.44.1",
"@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-commonjs": "^25.0.8",
"@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-terser": "^0.4.4",
"@webreflection/toml-j0.4": "^1.1.3", "@webreflection/toml-j0.4": "^1.1.3",
"@xterm/addon-fit": "^0.9.0-beta.1", "@xterm/addon-fit": "^0.10.0",
"chokidar": "^3.5.3", "@xterm/addon-web-links": "^0.11.0",
"bun": "^1.1.12",
"chokidar": "^3.6.0",
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"eslint": "^8.56.0", "eslint": "^9.4.0",
"rollup": "^4.9.6", "rollup": "^4.18.0",
"rollup-plugin-postcss": "^4.0.2", "rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-string": "^3.0.0", "rollup-plugin-string": "^3.0.0",
"static-handler": "^0.4.3", "static-handler": "^0.4.3",
"typescript": "^5.3.3", "typescript": "^5.4.5",
"xterm": "^5.3.0", "xterm": "^5.3.0",
"xterm-readline": "^1.1.1" "xterm-readline": "^1.1.1"
}, },

View File

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

View File

@@ -42,3 +42,34 @@ mpy-config {
.mpy-editor-run-button:disabled { .mpy-editor-run-button:disabled {
opacity: 1; 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

@@ -24,15 +24,51 @@ import sync from "./sync.js";
import bootstrapNodeAndPlugins from "./plugins-helper.js"; import bootstrapNodeAndPlugins from "./plugins-helper.js";
import { ErrorCode } from "./exceptions.js"; import { ErrorCode } from "./exceptions.js";
import { robustFetch as fetch, getText } from "./fetch.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 // generic helper to disambiguate between custom element and script
const isScript = ({ tagName }) => tagName === "SCRIPT"; const isScript = ({ tagName }) => tagName === "SCRIPT";
// Used to create either Pyodide or MicroPython workers
// with the PyScript module available within the code
const [PyWorker, MPWorker] = [...TYPES.entries()].map(
([TYPE, interpreter]) =>
/**
* A `Worker` facade able to bootstrap on the worker thread only a PyScript module.
* @param {string} file the python file to run ina worker.
* @param {{config?: string | object, async?: boolean}} [options] optional configuration for the worker.
* @returns {Promise<Worker & {sync: object}>}
*/
async function PyScriptWorker(file, options) {
await configs.get(TYPE).plugins;
const xworker = XWorker.call(
new Hook(null, hooked.get(TYPE)),
file,
{
...options,
type: interpreter,
},
);
assign(xworker.sync, sync);
return xworker.ready;
},
);
// avoid multiple initialization of the same library // avoid multiple initialization of the same library
const [ const [
{ {
PyWorker: exportedPyWorker, PyWorker: exportedPyWorker,
MPWorker: exportedMPWorker,
hooks: exportedHooks, hooks: exportedHooks,
config: exportedConfig, config: exportedConfig,
whenDefined: exportedWhenDefined, whenDefined: exportedWhenDefined,
@@ -40,6 +76,7 @@ const [
alreadyLive, alreadyLive,
] = stickyModule("@pyscript/core", { ] = stickyModule("@pyscript/core", {
PyWorker, PyWorker,
MPWorker,
hooks, hooks,
config: {}, config: {},
whenDefined, whenDefined,
@@ -48,11 +85,15 @@ const [
export { export {
TYPES, TYPES,
exportedPyWorker as PyWorker, exportedPyWorker as PyWorker,
exportedMPWorker as MPWorker,
exportedHooks as hooks, exportedHooks as hooks,
exportedConfig as config, exportedConfig as config,
exportedWhenDefined as whenDefined, exportedWhenDefined as whenDefined,
}; };
export const offline_interpreter = (config) =>
config?.interpreter && new URL(config.interpreter, location.href).href;
const hooked = new Map(); const hooked = new Map();
for (const [TYPE, interpreter] of TYPES) { for (const [TYPE, interpreter] of TYPES) {
@@ -116,6 +157,7 @@ for (const [TYPE, interpreter] of TYPES) {
// enrich the Python env with some JS utility for main // enrich the Python env with some JS utility for main
interpreter.registerJsModule("_pyscript", { interpreter.registerJsModule("_pyscript", {
PyWorker, PyWorker,
js_import: (...urls) => Promise.all(urls.map((url) => import(url))),
get target() { get target() {
return isScript(currentElement) return isScript(currentElement)
? currentElement.target.id ? currentElement.target.id
@@ -137,7 +179,7 @@ for (const [TYPE, interpreter] of TYPES) {
// specific main and worker hooks // specific main and worker hooks
const hooks = { const hooks = {
main: { main: {
...codeFor(main), ...codeFor(main, TYPE),
async onReady(wrap, element) { async onReady(wrap, element) {
registerModule(wrap); registerModule(wrap);
@@ -234,7 +276,7 @@ for (const [TYPE, interpreter] of TYPES) {
}, },
}, },
worker: { worker: {
...codeFor(worker), ...codeFor(worker, TYPE),
// these are lazy getters that returns a composition // these are lazy getters that returns a composition
// of the current hooks or undefined, if no hook is present // of the current hooks or undefined, if no hook is present
get onReady() { get onReady() {
@@ -263,7 +305,7 @@ for (const [TYPE, interpreter] of TYPES) {
interpreter, interpreter,
hooks, hooks,
env: `${TYPE}-script`, env: `${TYPE}-script`,
version: config?.interpreter, version: offline_interpreter(config),
onerror(error, element) { onerror(error, element) {
errors.set(element, error); errors.set(element, error);
}, },
@@ -314,24 +356,3 @@ for (const [TYPE, interpreter] of TYPES) {
// export the used config without allowing leaks through it // export the used config without allowing leaks through it
exportedConfig[TYPE] = structuredClone(config); exportedConfig[TYPE] = structuredClone(config);
} }
/**
* A `Worker` facade able to bootstrap on the worker thread only a PyScript module.
* @param {string} file the python file to run ina worker.
* @param {{config?: string | object, async?: boolean}} [options] optional configuration for the worker.
* @returns {Worker & {sync: ProxyHandler<object>}}
*/
function PyWorker(file, options) {
const hooks = hooked.get("py");
// this propagates pyscript worker hooks without needing a pyscript
// bootstrap + it passes arguments and it defaults to `pyodide`
// as the interpreter to use in the worker, as all hooks assume that
// and as `pyodide` is the only default interpreter that can deal with
// all the features we need to deliver pyscript out there.
const xworker = XWorker.call(new Hook(null, hooks), file, {
type: "pyodide",
...options,
});
assign(xworker.sync, sync);
return xworker;
}

View File

@@ -1,7 +1,10 @@
import { FetchError, ErrorCode } from "./exceptions.js"; import { FetchError, ErrorCode } from "./exceptions.js";
import { getText } from "polyscript/exports";
export { getText }; /**
* @param {Response} response
* @returns
*/
export const getText = (response) => response.text();
/** /**
* This is a fetch wrapper that handles any non 200 responses and throws a * This is a fetch wrapper that handles any non 200 responses and throws a

View File

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

View File

@@ -1,6 +1,6 @@
// PyScript py-editor plugin // PyScript py-editor plugin
import { Hook, XWorker, dedent } from "polyscript/exports"; import { Hook, XWorker, dedent, defineProperties } from "polyscript/exports";
import { TYPES } from "../core.js"; import { TYPES, offline_interpreter, 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>`; 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 getID = (type) => `${type}-editor-${id++}`;
const envs = new Map(); const envs = new Map();
const configs = new Map();
const hooks = { const hooks = {
worker: { worker: {
codeBeforeRun: () => stdlib,
// works on both Pyodide and MicroPython // works on both Pyodide and MicroPython
onReady: ({ runAsync, io }, { sync }) => { onReady: ({ runAsync, io }, { sync }) => {
io.stdout = (line) => sync.write(line); io.stdout = io.buffered(sync.write);
io.stderr = (line) => sync.writeErr(line); io.stderr = io.buffered(sync.writeErr);
sync.revoke(); sync.revoke();
sync.runAsync = runAsync; sync.runAsync = runAsync;
}, },
@@ -23,15 +25,29 @@ const hooks = {
async function execute({ currentTarget }) { async function execute({ currentTarget }) {
const { env, pySrc, outDiv } = this; const { env, pySrc, outDiv } = this;
const hasRunButton = !!currentTarget;
if (hasRunButton) {
currentTarget.disabled = true; currentTarget.disabled = true;
outDiv.innerHTML = ""; outDiv.innerHTML = "";
}
if (!envs.has(env)) { if (!envs.has(env)) {
const srcLink = URL.createObjectURL(new Blob([""])); const srcLink = URL.createObjectURL(new Blob([""]));
const xworker = XWorker.call(new Hook(null, hooks), srcLink, { const details = { type: this.interpreter };
type: this.interpreter, const { config } = this;
}); if (config) {
details.configURL = config;
const { parse } = config.endsWith(".toml")
? await import(/* webpackIgnore: true */ "../3rd-party/toml.js")
: JSON;
details.config = parse(await fetch(config).then((r) => r.text()));
details.version = offline_interpreter(details.config);
} else {
details.config = {};
}
const xworker = XWorker.call(new Hook(null, hooks), srcLink, details);
const { sync } = xworker; const { sync } = xworker;
const { promise, resolve } = Promise.withResolvers(); const { promise, resolve } = Promise.withResolvers();
@@ -44,23 +60,27 @@ async function execute({ currentTarget }) {
// wait for the env then set the target div // wait for the env then set the target div
// before executing the current code // before executing the current code
envs.get(env).then((xworker) => { return envs.get(env).then((xworker) => {
xworker.onerror = ({ error }) => { xworker.onerror = ({ error }) => {
if (hasRunButton) {
outDiv.innerHTML += `<span style='color:red'>${ outDiv.innerHTML += `<span style='color:red'>${
error.message || error error.message || error
}</span>\n`; }</span>\n`;
}
console.error(error); console.error(error);
}; };
const enable = () => { const enable = () => {
currentTarget.disabled = false; if (hasRunButton) currentTarget.disabled = false;
}; };
const { sync } = xworker; const { sync } = xworker;
sync.write = (str) => { sync.write = (str) => {
outDiv.innerText += `${str}\n`; if (hasRunButton) outDiv.innerText += `${str}\n`;
}; };
sync.writeErr = (str) => { sync.writeErr = (str) => {
if (hasRunButton) {
outDiv.innerHTML += `<span style='color:red'>${str}</span>\n`; outDiv.innerHTML += `<span style='color:red'>${str}</span>\n`;
}
}; };
sync.runAsync(pySrc).then(enable, enable); sync.runAsync(pySrc).then(enable, enable);
}); });
@@ -120,7 +140,6 @@ const init = async (script, type, interpreter) => {
{ keymap }, { keymap },
{ defaultKeymap }, { defaultKeymap },
] = await Promise.all([ ] = await Promise.all([
// TODO: find a way to actually produce these bundles locally
import(/* webpackIgnore: true */ "../3rd-party/codemirror.js"), import(/* webpackIgnore: true */ "../3rd-party/codemirror.js"),
import(/* webpackIgnore: true */ "../3rd-party/codemirror_state.js"), import(/* webpackIgnore: true */ "../3rd-party/codemirror_state.js"),
import( import(
@@ -131,9 +150,90 @@ const init = async (script, type, interpreter) => {
import(/* webpackIgnore: true */ "../3rd-party/codemirror_commands.js"), import(/* webpackIgnore: true */ "../3rd-party/codemirror_commands.js"),
]); ]);
const selector = script.getAttribute("target"); let isSetup = script.hasAttribute("setup");
const hasConfig = script.hasAttribute("config");
const env = `${interpreter}-${script.getAttribute("env") || getID(type)}`;
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 = {
interpreter,
env,
config:
hasConfig &&
new URL(script.getAttribute("config"), location.href).href,
get pySrc() {
return isSetup ? source : editor.state.doc.toString();
},
get outDiv() {
return isSetup ? null : outDiv;
},
};
let target; let target;
defineProperties(script, {
target: { get: () => target },
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 execute
.call(context, { currentTarget: null })
.then(restore, restore);
},
},
});
const notify = () => {
const event = new Event(`${type}-editor`, { bubbles: true });
script.dispatchEvent(event);
};
if (isSetup) {
await execute.call(context, { currentTarget: null });
notify();
return;
}
const selector = script.getAttribute("target");
if (selector) { if (selector) {
target = target =
document.getElementById(selector) || document.getElementById(selector) ||
@@ -149,18 +249,6 @@ const init = async (script, type, interpreter) => {
if (!target.hasAttribute("exec-id")) target.setAttribute("exec-id", 0); if (!target.hasAttribute("exec-id")) target.setAttribute("exec-id", 0);
if (!target.hasAttribute("root")) target.setAttribute("root", target.id); if (!target.hasAttribute("root")) target.setAttribute("root", target.id);
const env = `${interpreter}-${script.getAttribute("env") || getID(type)}`;
const context = {
interpreter,
env,
get pySrc() {
return editor.state.doc.toString();
},
get outDiv() {
return outDiv;
},
};
// @see https://github.com/JeffersGlass/mkdocs-pyscript/blob/main/mkdocs_pyscript/js/makeblocks.js // @see https://github.com/JeffersGlass/mkdocs-pyscript/blob/main/mkdocs_pyscript/js/makeblocks.js
const listener = execute.bind(context); const listener = execute.bind(context);
const [boxDiv, outDiv] = makeBoxDiv(listener, type); const [boxDiv, outDiv] = makeBoxDiv(listener, type);
@@ -195,6 +283,7 @@ const init = async (script, type, interpreter) => {
}); });
editor.focus(); editor.focus();
notify();
}; };
// avoid too greedy MutationObserver operations at distance // avoid too greedy MutationObserver operations at distance
@@ -210,7 +299,7 @@ const resetTimeout = () => {
}; };
// triggered both ASAP on the living DOM and via MutationObserver later // triggered both ASAP on the living DOM and via MutationObserver later
const pyEditor = async () => { const pyEditor = () => {
if (timeout) return; if (timeout) return;
timeout = setTimeout(resetTimeout, 250); timeout = setTimeout(resetTimeout, 250);
for (const [type, interpreter] of TYPES) { for (const [type, interpreter] of TYPES) {

View File

@@ -1,11 +1,13 @@
// PyScript py-terminal plugin // PyScript py-terminal plugin
import { TYPES, hooks } from "../core.js"; import { TYPES } from "../core.js";
import { notify } from "./error.js"; import { notify } from "./error.js";
import { defineProperty } from "polyscript/exports"; import { customObserver } from "polyscript/exports";
const SELECTOR = [...TYPES.keys()] // will contain all valid selectors
.map((type) => `script[type="${type}"][terminal],${type}-script[terminal]`) const SELECTORS = [];
.join(",");
// avoid processing same elements twice
const processed = new WeakSet();
// show the error on main and // show the error on main and
// stops the module from keep executing // stops the module from keep executing
@@ -14,60 +16,16 @@ const notifyAndThrow = (message) => {
throw new Error(message); throw new Error(message);
}; };
const notParsedYet = (script) => !bootstrapped.has(script);
const onceOnMain = ({ attributes: { worker } }) => !worker; const onceOnMain = ({ attributes: { worker } }) => !worker;
const bootstrapped = new WeakSet();
let addStyle = true; let addStyle = true;
// this callback will be serialized as string and it never needs for (const type of TYPES.keys()) {
// to be invoked multiple times. Each xworker here is bootstrapped const selector = `script[type="${type}"][terminal],${type}-script[terminal]`;
// only once thanks to the `sync.is_pyterminal()` check. SELECTORS.push(selector);
const workerReady = ({ interpreter, io, run }, { sync }) => { customObserver.set(selector, async (element) => {
if (!sync.is_pyterminal()) return; // we currently support only one terminal on main as in "classic"
const terminals = document.querySelectorAll(SELECTORS.join(","));
// 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) if ([].filter.call(terminals, onceOnMain).length > 1)
notifyAndThrow("You can use at most 1 main terminal"); notifyAndThrow("You can use at most 1 main terminal");
@@ -82,125 +40,21 @@ const pyTerminal = async () => {
); );
} }
// lazy load these only when a valid terminal is found if (processed.has(element)) return;
const [{ Terminal }, { Readline }, { FitAddon }] = await Promise.all([ processed.add(element);
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) { const bootstrap = (module) => module.default(element);
// hopefully to be removed in the near future!
if (element.matches('script[type="mpy"],mpy-script'))
notifyAndThrow("Unsupported terminal.");
const readline = new Readline(); // we can't be smart with template literals for the dynamic import
// or bundlers are incapable of producing multiple files around
// common main thread initialization for both worker if (type === "mpy") {
// or main case, bootstrapping the terminal on its target await import(/* webpackIgnore: true */ "./py-terminal/mpy.js").then(
const init = (options) => { bootstrap,
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 { } else {
target = document.createElement("py-terminal"); await import(/* webpackIgnore: true */ "./py-terminal/py.js").then(
target.style.display = "block"; bootstrap,
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`);
};
});
}
}
};
const mo = new MutationObserver(pyTerminal);
mo.observe(document, { childList: true, subtree: true });
// try to check the current document ASAP
export default pyTerminal();

View File

@@ -0,0 +1,239 @@
// 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);
pyterminal_ready();
interpreter.replInit();
},
});
};
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) {
readChunks += 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"; 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 { entries } = Object;
const python = [ const python = [
@@ -16,16 +37,19 @@ const python = [
"_path = None", "_path = None",
]; ];
const ignore = new Ignore(python, "-");
const write = (base, literal) => { const write = (base, literal) => {
for (const [key, value] of entries(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") { if (typeof value === "string") {
const code = JSON.stringify(value); const code = JSON.stringify(value);
python.push(`_path.write_text(${code})`); ignore.push(`_path.write_text(${code},encoding="utf-8")`);
} else { } else {
// @see https://github.com/pyscript/pyscript/pull/1813#issuecomment-1781502909 // @see https://github.com/pyscript/pyscript/pull/1813#issuecomment-1781502909
python.push(`if not _os.path.exists("${base}/${key}"):`); ignore.push(`if not _os.path.exists("${base}/${key}"):`);
python.push(" _path.mkdir(parents=True, exist_ok=True)"); ignore.push(" _path.mkdir(parents=True, exist_ok=True)");
write(`${base}/${key}`, value); write(`${base}/${key}`, value);
} }
} }
@@ -42,4 +66,5 @@ python.push(
); );
python.push("\n"); python.push("\n");
export default python.join("\n"); export const stdlib = python.join("\n");
export const optional = ignore.join("\n");

View File

@@ -29,16 +29,21 @@
# pyscript.magic_js. This is the blessed way to access them from pyscript, # 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. # 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.display import HTML, display
from pyscript.fetch import fetch
from pyscript.magic_js import ( from pyscript.magic_js import (
RUNNING_IN_WORKER, RUNNING_IN_WORKER,
PyWorker, PyWorker,
config,
current_target, current_target,
document, document,
js_import,
js_modules, js_modules,
sync, sync,
window, window,
) )
from pyscript.websocket import WebSocket
try: try:
from pyscript.event_handling import when from pyscript.event_handling import when

View File

@@ -6,17 +6,17 @@ import re
from pyscript.magic_js import current_target, document, window from pyscript.magic_js import current_target, document, window
_MIME_METHODS = { _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", "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 format_dict = mimebundle
output, not_available = None, [] 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: if mime_type in format_dict:
output = format_dict[mime_type] output = format_dict[mime_type]
else: else:

View File

@@ -47,13 +47,14 @@ def when(event_type=None, selector=None):
wrapper = func wrapper = func
except AttributeError: except AttributeError:
# TODO: this is currently an quick hack to get micropython working but we need # TODO: this is very ugly hack to get micropython working because inspect.signature
# to actually properly replace inspect.signature with something else # 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): def wrapper(*args, **kwargs):
try: try:
return func(*args, **kwargs) return func(*args, **kwargs)
except TypeError as e: 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() return func()
raise raise

View File

@@ -0,0 +1,87 @@
import json
import js
from pyscript.util import as_bytearray
### 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)
# exposed methods with Pythonic results
async def arrayBuffer(self):
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))
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()
async def blob(self):
response = await self._response()
return await response.blob()
async def bytearray(self):
response = await self._response()
return await response.bytearray()
async def json(self):
response = await self._response()
return await response.json()
async def text(self):
response = await self._response()
return await response.text()
def 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

@@ -1,11 +1,15 @@
import json
import sys import sys
import js as globalThis import js as globalThis
from polyscript import config as _config
from polyscript import js_modules from polyscript import js_modules
from pyscript.util import NotSupported from pyscript.util import NotSupported
RUNNING_IN_WORKER = not hasattr(globalThis, "document") RUNNING_IN_WORKER = not hasattr(globalThis, "document")
config = json.loads(globalThis.JSON.stringify(_config))
# allow `from pyscript.js_modules.xxx import yyy` # allow `from pyscript.js_modules.xxx import yyy`
class JSModule: class JSModule:
@@ -24,16 +28,37 @@ for name in globalThis.Reflect.ownKeys(js_modules):
sys.modules["pyscript.js_modules"] = js_modules sys.modules["pyscript.js_modules"] = js_modules
if RUNNING_IN_WORKER: if RUNNING_IN_WORKER:
import js
import polyscript import polyscript
PyWorker = NotSupported( PyWorker = NotSupported(
"pyscript.PyWorker", "pyscript.PyWorker",
"pyscript.PyWorker works only when running in the main thread", "pyscript.PyWorker works only when running in the main thread",
) )
try:
globalThis.SharedArrayBuffer.new(4)
import js
window = polyscript.xworker.window window = polyscript.xworker.window
document = window.document document = window.document
js.document = 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",
)
sync = polyscript.xworker.sync sync = polyscript.xworker.sync
# in workers the display does not have a default ID # in workers the display does not have a default ID
@@ -43,7 +68,7 @@ if RUNNING_IN_WORKER:
else: else:
import _pyscript import _pyscript
from _pyscript import PyWorker from _pyscript import PyWorker, js_import
window = globalThis window = globalThis
document = globalThis.document document = globalThis.document

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: class NotSupported:
""" """
Small helper that raises exceptions if you try to get/set any attribute on Small helper that raises exceptions if you try to get/set any attribute on

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

@@ -1 +1,2 @@
from .pydom import JSProperty
from .pydom import dom as pydom from .pydom import dom as pydom

View File

@@ -1,3 +1,5 @@
import inspect
try: try:
from typing import Any from typing import Any
except ImportError: except ImportError:
@@ -34,6 +36,23 @@ from pyscript import display, document, window
alert = window.alert alert = window.alert
class JSProperty:
"""JS property descriptor that directly maps to the property with the same
name in the underlying JS component."""
def __init__(self, name: str, allow_nones: bool = False):
self.name = name
self.allow_nones = allow_nones
def __get__(self, obj, objtype=None):
return getattr(obj._js, self.name)
def __set__(self, obj, value):
if not self.allow_nones and value is None:
return
setattr(obj._js, self.name, value)
class BaseElement: class BaseElement:
def __init__(self, js_element): def __init__(self, js_element):
self._js = js_element self._js = js_element
@@ -104,7 +123,7 @@ class Element(BaseElement):
# TODO: this is Pyodide specific for now!!!!!! # TODO: this is Pyodide specific for now!!!!!!
# if we get passed a JSProxy Element directly we just map it to the # if we get passed a JSProxy Element directly we just map it to the
# higher level Python element # higher level Python element
if isinstance(child, JsProxy): if inspect.isclass(JsProxy) and isinstance(child, JsProxy):
return self.append(Element(child)) return self.append(Element(child))
elif isinstance(child, Element): elif isinstance(child, Element):
@@ -125,6 +144,14 @@ class Element(BaseElement):
def html(self, value): def html(self, value):
self._js.innerHTML = value self._js.innerHTML = value
@property
def text(self):
return self._js.textContent
@text.setter
def text(self, value):
self._js.textContent = value
@property @property
def content(self): def content(self):
# TODO: This breaks with with standard template elements. Define how to best # TODO: This breaks with with standard template elements. Define how to best
@@ -259,7 +286,7 @@ class Element(BaseElement):
canvas._js.width = width canvas._js.width = width
canvas._js.height = height canvas._js.height = height
elif isistance(to, Element): elif isinstance(to, Element):
if to._js.tagName != "CANVAS": if to._js.tagName != "CANVAS":
raise TypeError("Element to snap to must a canvas.") raise TypeError("Element to snap to must a canvas.")
canvas = to canvas = to

View File

@@ -0,0 +1 @@
from . import elements

View File

@@ -0,0 +1,947 @@
import inspect
import sys
from pyscript import document, when, window
from pyweb import JSProperty, pydom
#: A flag to show if MicroPython is the current Python interpreter.
is_micropython = "MicroPython" in sys.version
def getmembers_static(cls):
"""Cross-interpreter implementation of inspect.getmembers_static."""
if is_micropython: # pragma: no cover
return [(name, getattr(cls, name)) for name, _ in inspect.getmembers(cls)]
return inspect.getmembers_static(cls)
class ElementBase(pydom.Element):
tag = "div"
# GLOBAL ATTRIBUTES
# These are attribute that all elements have (this list is a subset of the official one)
# We are trying to capture the most used ones
accesskey = JSProperty("accesskey")
autofocus = JSProperty("autofocus")
autocapitalize = JSProperty("autocapitalize")
className = JSProperty("className")
contenteditable = JSProperty("contenteditable")
draggable = JSProperty("draggable")
enterkeyhint = JSProperty("enterkeyhint")
hidden = JSProperty("hidden")
id = JSProperty("id")
lang = JSProperty("lang")
nonce = JSProperty("nonce")
part = JSProperty("part")
popover = JSProperty("popover")
slot = JSProperty("slot")
spellcheck = JSProperty("spellcheck")
tabindex = JSProperty("tabindex")
title = JSProperty("title")
translate = JSProperty("translate")
virtualkeyboardpolicy = JSProperty("virtualkeyboardpolicy")
def __init__(self, style=None, **kwargs):
super().__init__(document.createElement(self.tag))
# set all the style properties provided in input
if isinstance(style, dict):
for key, value in style.items():
self.style[key] = value
elif style is None:
pass
else:
raise ValueError(
f"Style should be a dictionary, received {style} (type {type(style)}) instead."
)
# IMPORTANT!!! This is used to auto-harvest all input arguments and set them as properties
self._init_properties(**kwargs)
def _init_properties(self, **kwargs):
"""Set all the properties (of type JSProperties) provided in input as properties
of the class instance.
Args:
**kwargs: The properties to set
"""
# Look at all the properties of the class and see if they were provided in kwargs
for attr_name, attr in getmembers_static(self.__class__):
# For each one, actually check if it is a property of the class and set it
if isinstance(attr, JSProperty) and attr_name in kwargs:
try:
setattr(self, attr_name, kwargs[attr_name])
except Exception as e:
print(f"Error setting {attr_name} to {kwargs[attr_name]}: {e}")
raise
class TextElementBase(ElementBase):
def __init__(self, content=None, style=None, **kwargs):
super().__init__(style=style, **kwargs)
# If it's an element, append the element
if isinstance(content, pydom.Element):
self.append(content)
# If it's a list of elements
elif isinstance(content, list):
for item in content:
self.append(item)
# If the content wasn't set just ignore
elif content is None:
pass
else:
# Otherwise, set content as the html of the element
self.html = content
# IMPORTANT: For all HTML components defined below, we are not mapping all
# available attributes, just the global and the most common ones.
# If you need to access a specific attribute, you can always use the `_js.<attribute>`
class a(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a"""
tag = "a"
download = JSProperty("download")
href = JSProperty("href")
referrerpolicy = JSProperty("referrerpolicy")
rel = JSProperty("rel")
target = JSProperty("target")
type = JSProperty("type")
class abbr(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/abbr"""
tag = "abbr"
class address(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/address"""
tag = "address"
class area(ElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/area"""
tag = "area"
alt = JSProperty("alt")
coords = JSProperty("coords")
download = JSProperty("download")
href = JSProperty("href")
ping = JSProperty("ping")
referrerpolicy = JSProperty("referrerpolicy")
rel = JSProperty("rel")
shape = JSProperty("shape")
target = JSProperty("target")
class article(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/article"""
tag = "article"
class aside(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/aside"""
tag = "aside"
class audio(ElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/audio"""
tag = "audio"
autoplay = JSProperty("autoplay")
controls = JSProperty("controls")
controlslist = JSProperty("controlslist")
crossorigin = JSProperty("crossorigin")
disableremoteplayback = JSProperty("disableremoteplayback")
loop = JSProperty("loop")
muted = JSProperty("muted")
preload = JSProperty("preload")
src = JSProperty("src")
class b(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/b"""
tag = "b"
class blockquote(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/blockquote"""
tag = "blockquote"
cite = JSProperty("cite")
class br(ElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/br"""
tag = "br"
class button(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button"""
tag = "button"
autofocus = JSProperty("autofocus")
disabled = JSProperty("disabled")
form = JSProperty("form")
formaction = JSProperty("formaction")
formenctype = JSProperty("formenctype")
formmethod = JSProperty("formmethod")
formnovalidate = JSProperty("formnovalidate")
formtarget = JSProperty("formtarget")
name = JSProperty("name")
type = JSProperty("type")
value = JSProperty("value")
class canvas(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas"""
tag = "canvas"
height = JSProperty("height")
width = JSProperty("width")
class caption(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/caption"""
tag = "caption"
class cite(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/cite"""
tag = "cite"
class code(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/code"""
tag = "code"
class data(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/data"""
tag = "data"
value = JSProperty("value")
class datalist(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/datalist"""
tag = "datalist"
class dd(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dd"""
tag = "dd"
class del_(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/del"""
tag = "del"
cite = JSProperty("cite")
datetime = JSProperty("datetime")
class details(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details"""
tag = "details"
open = JSProperty("open")
class dialog(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog"""
tag = "dialog"
open = JSProperty("open")
class div(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div"""
tag = "div"
class dl(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dl"""
tag = "dl"
value = JSProperty("value")
class dt(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dt"""
tag = "dt"
class em(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/em"""
tag = "em"
class embed(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/embed"""
tag = "embed"
height = JSProperty("height")
src = JSProperty("src")
type = JSProperty("type")
width = JSProperty("width")
class fieldset(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/fieldset"""
tag = "fieldset"
disabled = JSProperty("disabled")
form = JSProperty("form")
name = JSProperty("name")
class figcaption(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/figcaption"""
tag = "figcaption"
class figure(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/figure"""
tag = "figure"
class footer(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/footer"""
tag = "footer"
class form(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form"""
tag = "form"
accept_charset = JSProperty("accept-charset")
action = JSProperty("action")
autocapitalize = JSProperty("autocapitalize")
autocomplete = JSProperty("autocomplete")
enctype = JSProperty("enctype")
name = JSProperty("name")
method = JSProperty("method")
nonvalidate = JSProperty("nonvalidate")
rel = JSProperty("rel")
target = JSProperty("target")
class h1(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h1"""
tag = "h1"
class h2(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h2"""
tag = "h2"
class h3(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h3"""
tag = "h3"
class h4(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h4"""
tag = "h4"
class h5(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h5"""
tag = "h5"
class h6(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/h6"""
tag = "h6"
class header(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/header"""
tag = "header"
class hgroup(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/hgroup"""
tag = "hgroup"
class hr(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/hr"""
tag = "hr"
class i(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/i"""
tag = "i"
class iframe(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe"""
tag = "iframe"
allow = JSProperty("allow")
allowfullscreen = JSProperty("allowfullscreen")
height = JSProperty("height")
loading = JSProperty("loading")
name = JSProperty("name")
referrerpolicy = JSProperty("referrerpolicy")
sandbox = JSProperty("sandbox")
src = JSProperty("src")
srcdoc = JSProperty("srcdoc")
width = JSProperty("width")
class img(ElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img"""
tag = "img"
alt = JSProperty("alt")
crossorigin = JSProperty("crossorigin")
decoding = JSProperty("decoding")
fetchpriority = JSProperty("fetchpriority")
height = JSProperty("height")
ismap = JSProperty("ismap")
loading = JSProperty("loading")
referrerpolicy = JSProperty("referrerpolicy")
sizes = JSProperty("sizes")
src = JSProperty("src")
width = JSProperty("width")
# NOTE: Input is a reserved keyword in Python, so we use input_ instead
class input_(ElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input"""
tag = "input"
accept = JSProperty("accept")
alt = JSProperty("alt")
autofocus = JSProperty("autofocus")
capture = JSProperty("capture")
checked = JSProperty("checked")
dirname = JSProperty("dirname")
disabled = JSProperty("disabled")
form = JSProperty("form")
formaction = JSProperty("formaction")
formenctype = JSProperty("formenctype")
formmethod = JSProperty("formmethod")
formnovalidate = JSProperty("formnovalidate")
formtarget = JSProperty("formtarget")
height = JSProperty("height")
list = JSProperty("list")
max = JSProperty("max")
maxlength = JSProperty("maxlength")
min = JSProperty("min")
minlength = JSProperty("minlength")
multiple = JSProperty("multiple")
name = JSProperty("name")
pattern = JSProperty("pattern")
placeholder = JSProperty("placeholder")
popovertarget = JSProperty("popovertarget")
popovertargetaction = JSProperty("popovertargetaction")
readonly = JSProperty("readonly")
required = JSProperty("required")
size = JSProperty("size")
src = JSProperty("src")
step = JSProperty("step")
type = JSProperty("type")
value = JSProperty("value")
width = JSProperty("width")
class ins(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ins"""
tag = "ins"
cite = JSProperty("cite")
datetime = JSProperty("datetime")
class kbd(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/kbd"""
tag = "kbd"
class label(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/label"""
tag = "label"
for_ = JSProperty("for")
class legend(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/legend"""
tag = "legend"
class li(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/li"""
tag = "li"
value = JSProperty("value")
class link(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link"""
tag = "link"
as_ = JSProperty("as")
crossorigin = JSProperty("crossorigin")
disabled = JSProperty("disabled")
fetchpriority = JSProperty("fetchpriority")
href = JSProperty("href")
imagesizes = JSProperty("imagesizes")
imagesrcset = JSProperty("imagesrcset")
integrity = JSProperty("integrity")
media = JSProperty("media")
rel = JSProperty("rel")
referrerpolicy = JSProperty("referrerpolicy")
sizes = JSProperty("sizes")
title = JSProperty("title")
type = JSProperty("type")
class main(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/main"""
tag = "main"
class map_(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/map"""
tag = "map"
name = JSProperty("name")
class mark(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/mark"""
tag = "mark"
class menu(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/menu"""
tag = "menu"
class meter(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meter"""
tag = "meter"
form = JSProperty("form")
high = JSProperty("high")
low = JSProperty("low")
max = JSProperty("max")
min = JSProperty("min")
optimum = JSProperty("optimum")
value = JSProperty("value")
class nav(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/nav"""
tag = "nav"
class object_(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/object"""
tag = "object"
data = JSProperty("data")
form = JSProperty("form")
height = JSProperty("height")
name = JSProperty("name")
type = JSProperty("type")
usemap = JSProperty("usemap")
width = JSProperty("width")
class ol(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ol"""
tag = "ol"
reversed = JSProperty("reversed")
start = JSProperty("start")
type = JSProperty("type")
class optgroup(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/optgroup"""
tag = "optgroup"
disabled = JSProperty("disabled")
label = JSProperty("label")
class option(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/option"""
tag = "option"
disabled = JSProperty("value")
label = JSProperty("label")
selected = JSProperty("selected")
value = JSProperty("value")
class output(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/output"""
tag = "output"
for_ = JSProperty("for")
form = JSProperty("form")
name = JSProperty("name")
class p(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/p"""
tag = "p"
class picture(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/picture"""
tag = "picture"
class pre(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/pre"""
tag = "pre"
class progress(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/progress"""
tag = "progress"
max = JSProperty("max")
value = JSProperty("value")
class q(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/q"""
tag = "q"
cite = JSProperty("cite")
class s(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/s"""
tag = "s"
class script(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script"""
tag = "script"
# Let's add async manually since it's a reserved keyword in Python
async_ = JSProperty("async")
blocking = JSProperty("blocking")
crossorigin = JSProperty("crossorigin")
defer = JSProperty("defer")
fetchpriority = JSProperty("fetchpriority")
integrity = JSProperty("integrity")
nomodule = JSProperty("nomodule")
nonce = JSProperty("nonce")
referrerpolicy = JSProperty("referrerpolicy")
src = JSProperty("src")
type = JSProperty("type")
class section(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/section"""
tag = "section"
class select(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select"""
tag = "select"
class small(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/small"""
tag = "small"
class source(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/source"""
tag = "source"
media = JSProperty("media")
sizes = JSProperty("sizes")
src = JSProperty("src")
srcset = JSProperty("srcset")
type = JSProperty("type")
class span(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/span"""
tag = "span"
class strong(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/strong"""
tag = "strong"
class style(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/style"""
tag = "style"
blocking = JSProperty("blocking")
media = JSProperty("media")
nonce = JSProperty("nonce")
title = JSProperty("title")
class sub(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/sub"""
tag = "sub"
class summary(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/summary"""
tag = "summary"
class sup(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/sup"""
tag = "sup"
class table(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/table"""
tag = "table"
class tbody(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/tbody"""
tag = "tbody"
class td(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/td"""
tag = "td"
colspan = JSProperty("colspan")
headers = JSProperty("headers")
rowspan = JSProperty("rowspan")
class template(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template"""
tag = "template"
shadowrootmode = JSProperty("shadowrootmode")
class textarea(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea"""
tag = "textarea"
autocapitalize = JSProperty("autocapitalize")
autocomplete = JSProperty("autocomplete")
autofocus = JSProperty("autofocus")
cols = JSProperty("cols")
dirname = JSProperty("dirname")
disabled = JSProperty("disabled")
form = JSProperty("form")
maxlength = JSProperty("maxlength")
minlength = JSProperty("minlength")
name = JSProperty("name")
placeholder = JSProperty("placeholder")
readonly = JSProperty("readonly")
required = JSProperty("required")
rows = JSProperty("rows")
spellcheck = JSProperty("spellcheck")
wrap = JSProperty("wrap")
class tfoot(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/tfoot"""
tag = "tfoot"
class th(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/th"""
tag = "th"
class thead(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/thead"""
tag = "thead"
class time(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/time"""
tag = "time"
datetime = JSProperty("datetime")
class title(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/title"""
tag = "title"
class tr(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/tr"""
tag = "tr"
abbr = JSProperty("abbr")
colspan = JSProperty("colspan")
headers = JSProperty("headers")
rowspan = JSProperty("rowspan")
scope = JSProperty("scope")
class track(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/track"""
tag = "track"
default = JSProperty("default")
kind = JSProperty("kind")
label = JSProperty("label")
src = JSProperty("src")
srclang = JSProperty("srclang")
class u(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/u"""
tag = "u"
class ul(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/ul"""
tag = "ul"
class var(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/var"""
tag = "var"
class video(TextElementBase):
"""Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video"""
tag = "video"
autoplay = JSProperty("autoplay")
controls = JSProperty("controls")
crossorigin = JSProperty("crossorigin")
disablepictureinpicture = JSProperty("disablepictureinpicture")
disableremoteplayback = JSProperty("disableremoteplayback")
height = JSProperty("height")
loop = JSProperty("loop")
muted = JSProperty("muted")
playsinline = JSProperty("playsinline")
poster = JSProperty("poster")
preload = JSProperty("preload")
src = JSProperty("src")
width = JSProperty("width")
# Custom Elements
class grid(TextElementBase):
tag = "div"
def __init__(self, layout, content=None, gap=None, **kwargs):
super().__init__(content, **kwargs)
self.style["display"] = "grid"
self.style["grid-template-columns"] = layout
# TODO: This should be a property
if not gap is None:
self.style["gap"] = gap

View File

@@ -0,0 +1,19 @@
<!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">
import { hooks } from "../dist/core.js";
hooks.main.codeBeforeRun.add('print(0)');
hooks.main.codeAfterRun.add('print(2)');
</script>
</head>
<body>
<script type="py">
# raise an error instead to see it on line 1
print(1)
</script>
</body>
</html>

View File

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

View File

@@ -0,0 +1,95 @@
<!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">
</head>
<body>
<script type="module">
import fetch from 'https://esm.run/@webreflection/fetch';
globalThis.fetch_text = await fetch("config.json").text();
globalThis.fetch_json = JSON.stringify(await fetch("config.json").json());
globalThis.fetch_buffer = new Uint8Array((await fetch("config.json").arrayBuffer())).length;
document.head.appendChild(
Object.assign(
document.createElement('script'),
{
type: 'module',
src: '../dist/core.js'
}
)
);
</script>
<script type="mpy" 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('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>
</html>

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

@@ -78,3 +78,13 @@ test('Pyodide + multiple terminals via Worker', async ({ page }) => {
await page.goto('http://localhost:8080/test/py-terminals.html'); await page.goto('http://localhost:8080/test/py-terminals.html');
await page.waitForSelector('html.first.second'); await page.waitForSelector('html.first.second');
}); });
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');
});

View File

@@ -0,0 +1,23 @@
<!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">
import { PyWorker } from '../../dist/core.js';
const { sync } = await PyWorker(
'./worker.py',
{
config: {
sync_main_only: true
}
}
);
document.documentElement.classList.add(
await sync.get_class()
);
</script>
</head>
</html>

View File

@@ -0,0 +1,3 @@
from pyscript import sync
sync.get_class = lambda: "ok"

View File

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

View File

@@ -0,0 +1,55 @@
<!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">
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>
<!-- 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">
from pyscript.js_modules.html_escaper import escape, unescape
print(unescape(escape("<OK>")))
a = 1
</script>
<!-- a share-nothing micropython editor -->
<script type="mpy-editor" config="./config.toml">
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(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,5 @@
from pyscript import window
window.console.log("OK")
a = 1

View File

@@ -10,6 +10,6 @@
</head> </head>
<body> <body>
<script type="py" src="terminal.py" worker terminal></script> <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> </body>
</html> </html>

View File

@@ -9,24 +9,10 @@
<style>.xterm { padding: .5rem; }</style> <style>.xterm { padding: .5rem; }</style>
</head> </head>
<body> <body>
<script type="py"> <script type="mpy" worker terminal>
def greetings(event): print("µpython")
print('hello world') import code
code.interact()
</script> </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> </body>
</html> </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,7 @@
<script type="module" src="../dist/core.js"></script> <script type="module" src="../dist/core.js"></script>
</head> </head>
<body> <body>
<script type="py" src="pydom.py"></script> <script type="mpy" src="pydom.py"></script>
<button id="just-a-button">Click For Time</button> <button id="just-a-button">Click For Time</button>
<button id="color-button">Click For Color</button> <button id="color-button">Click For Color</button>

View File

@@ -1,10 +1,13 @@
import random import random
import sys
import time import time
from datetime import datetime as dt from datetime import datetime as dt
from pyscript import display, when from pyscript import display, when
from pyweb import pydom from pyweb import pydom
display(sys.version, target="system-info")
@when("click", "#just-a-button") @when("click", "#just-a-button")
def on_click(): def on_click():

View File

@@ -10,6 +10,8 @@
<body> <body>
<script type="mpy" 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="just-a-button">Click For Time</button>
<button id="color-button">Click For Color</button> <button id="color-button">Click For Color</button>
<button id="color-reset-button">Reset Color</button> <button id="color-reset-button">Reset Color</button>

View File

@@ -98,6 +98,8 @@
<p class="collection"></p> <p class="collection"></p>
<div class="collection"></div> <div class="collection"></div>
<h3 class="collection"></h3> <h3 class="collection"></h3>
<div id="element_attribute_tests"></div>
</div> </div>

View File

@@ -163,6 +163,30 @@ class TestElement:
assert called assert called
def test_html_attribute(self):
# GIVEN an existing element on the page with a known empty text content
div = pydom["#element_attribute_tests"][0]
# WHEN we set the html attribute
div.html = "<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"
def test_text_attribute(self):
# GIVEN an existing element on the page with a known empty text content
div = pydom["#element_attribute_tests"][0]
# WHEN we set the html attribute
div.text = "<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>"
class TestCollection: class TestCollection:
def test_iter_eq_children(self): def test_iter_eq_children(self):

View File

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

View File

@@ -0,0 +1,605 @@
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 pyweb import pydom
from pyweb.ui.elements import {el_type}
el = {el_type}({attributes})
pydom.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("aside", "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,
)

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,16 +1,32 @@
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 TYPES from "./types.js";
/** /**
* A `Worker` facade able to bootstrap on the worker thread only a PyScript module. * A `Worker` facade able to bootstrap on the worker thread only a PyScript module.
* @param {string} file the python file to run ina worker. * @param {string} file the python file to run ina worker.
* @param {{config?: string | object, async?: boolean}} [options] optional configuration for the worker. * @param {{config?: string | object, async?: boolean}} [options] optional configuration for the worker.
* @returns {Worker & {sync: ProxyHandler<object>}} * @returns {Promise<Worker & {sync: object}>}
*/ */
declare function exportedPyWorker(file: string, options?: { declare function exportedPyWorker(file: string, options?: {
config?: string | object; config?: string | object;
async?: boolean; async?: boolean;
}): Worker & { }): Promise<Worker & {
sync: ProxyHandler<object>; sync: object;
}; }>;
/**
* A `Worker` facade able to bootstrap on the worker thread only a PyScript module.
* @param {string} file the python file to run ina worker.
* @param {{config?: string | object, async?: boolean}} [options] optional configuration for the worker.
* @returns {Promise<Worker & {sync: object}>}
*/
declare function exportedMPWorker(file: string, options?: {
config?: string | object;
async?: boolean;
}): Promise<Worker & {
sync: object;
}>;
declare const exportedHooks: { declare const exportedHooks: {
main: { main: {
onWorker: Set<Function>; onWorker: Set<Function>;
@@ -38,5 +54,4 @@ declare const exportedHooks: {
}; };
declare const exportedConfig: {}; declare const exportedConfig: {};
declare const exportedWhenDefined: (type: string) => Promise<any>; declare const exportedWhenDefined: (type: string) => Promise<any>;
import sync from "./sync.js"; export { stdlib, optional, inputFailure, TYPES, exportedPyWorker as PyWorker, exportedMPWorker as MPWorker, exportedHooks as hooks, exportedConfig as config, exportedWhenDefined as whenDefined };
export { TYPES, exportedPyWorker as PyWorker, exportedHooks as hooks, exportedConfig as config, exportedWhenDefined as whenDefined };

View File

@@ -8,5 +8,4 @@
* @returns {Promise<Response>} * @returns {Promise<Response>}
*/ */
export function robustFetch(url: string, options?: Request): Promise<Response>; export function robustFetch(url: string, options?: Request): Promise<Response>;
export { getText }; export function getText(response: Response): Promise<string>;
import { getText } from "polyscript/exports";

View File

@@ -1,7 +1,8 @@
export function main(name: any): any; export function main(name: any): any;
export function worker(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 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 { export namespace hooks {
namespace main { namespace main {
let onWorker: Set<Function>; let onWorker: Set<Function>;

View File

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

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;

View File

@@ -1,2 +1,2 @@
declare const _default: string; export const stdlib: string;
export default _default; export const optional: string;

View File

@@ -3,13 +3,20 @@ declare namespace _default {
"__init__.py": string; "__init__.py": string;
"display.py": string; "display.py": string;
"event_handling.py": string; "event_handling.py": string;
"fetch.py": string;
"ffi.py": string;
"magic_js.py": string; "magic_js.py": string;
"util.py": string; "util.py": string;
"websocket.py": string;
}; };
let pyweb: { let pyweb: {
"__init__.py": string; "__init__.py": string;
"media.py": string; "media.py": string;
"pydom.py": string; "pydom.py": string;
ui: {
"__init__.py": string;
"elements.py": string;
};
}; };
} }
export default _default; export default _default;

View File

@@ -1,13 +1,13 @@
black==23.11.0 black==24.4.2
isort==5.12.0 isort==5.13.2
pytest==7.1.2 pytest==8.2.1
pre-commit==3.5.0 pre-commit==3.7.1
playwright==1.33.0 playwright==1.44.0
pytest-playwright==0.3.3 pytest-playwright==0.5.0
pytest-xdist==3.3.0 pytest-xdist==3.6.1
pexpect==4.9.0 pexpect==4.9.0
pyodide_py==0.24.1 pyodide_py==0.24.1
micropip==0.5.0 micropip==0.5.0
toml==0.10.2 toml==0.10.2
numpy==1.26.2 numpy==1.26.4
pillow==10.1.0 pillow==10.3.0