Compare commits

...

117 Commits

Author SHA1 Message Date
Ted Patrick
5f319452d5 simplify OutputCtxManager logging (#809)
* simplify OutputCtxManager logging

* [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>
2022-09-30 09:41:53 -05:00
Madhur Tandon
60d505d2d1 update docs for the minimal config (#807) 2022-09-30 19:35:16 +05:30
Ted Patrick
f64cc4dcae Minimum viable py-config default schema (#803)
* Minimium viable py-config default schema

* Add packages and paths to default

* Add in default for plugins

* Remove tests for values no longer in the default config
2022-09-29 18:54:56 -05:00
Madhur Tandon
60e6f4293a new docs for py-config without removing py-env (#802)
* new docs for py-config without removing py-env

* fix table

* fix table

* fix message for local modules

* fix src
2022-09-29 20:50:33 +05:30
Jeff Glass
00ab9a8d02 Update MAINTAINERS.md (#797) 2022-09-28 18:34:41 -05:00
Ted Patrick
7d5f6c9ead Loading and void script.evaluate() (#796)
* Loading and Script evaluate with Promise.all

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

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

* Move to 'void script.evaluate();'

* chmod files to 644

* Add a 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>
2022-09-28 17:38:21 -05:00
pre-commit-ci[bot]
a295edf19d [pre-commit.ci] pre-commit autoupdate (#794)
updates:
- [github.com/asottile/pyupgrade: v2.38.0 → v2.38.2](https://github.com/asottile/pyupgrade/compare/v2.38.0...v2.38.2)
- [github.com/pre-commit/mirrors-eslint: v8.23.1 → v8.24.0](https://github.com/pre-commit/mirrors-eslint/compare/v8.23.1...v8.24.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-09-28 18:52:34 +02:00
Fábio Rosado
b674515d06 Add more unit and integration tests (#773)
* Add unit tests for pyloader and pytitle

* Add more unit tests for pyrepl, pybox and pyinputbox

* Add more tests for pyscript and pyconfig

* White space

* Fix d3 tests and improve more examples test

* Update matplotlib test

* Add numpy to dependencies

* Address Madhur comments

* Update test name
2022-09-27 03:47:32 +05:30
Ted Patrick
d033ab04da Credit Pyodide within readme.md (#792)
* Credit pyodide within readme

* Use pyscript.net definition
2022-09-26 07:03:01 -05:00
Ted Patrick
304d76d088 Enable snapshot publishing by removing --dryrun (#790) 2022-09-26 05:41:13 -05:00
Ted Patrick
978afdad97 Snapshot releases (#789)
* A workflow that enables snapshot releases by manual github action

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

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

* typo in comment

* match naming and add name to workflow

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-09-25 12:24:41 -05:00
Ted Patrick
d4e41e679d examples: py-env to py-config (#784)
* examples: py-env to py-config

* 1 example as JSON with type

* Fix todo event attribute
2022-09-23 11:04:31 -05:00
Madhur Tandon
146264ff12 add toml support for configs with fast-toml (#783)
* add toml support for configs with fast-toml

* fix package-lock.json and pin pyodide to 0.21.2

* use browser version of fast-toml

* disable eslint and add credits

* fix jest issues

* use type attribute for py-config
2022-09-23 20:11:22 +05:30
Emir
dcb107ae65 add pure-python (async) HTTP request how to (#778)
* add HTTP request how to

First draft.
Proof read http-requests.md


Formatting


Reword intro


modify fetch API docs links


Conclusion

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

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

* add http-request howto into toctree, index

* implement review

By @madhur-tandon
Implement review

and fix formatting mistake in request function docstring.

* add # HTTP requests section, add fetch_kwargs to helper function

Add a section in the explanation discussing HTTP Requests

* add API reference

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

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

* specify docs for `pyodide.http` members

* address review

By @marimeireles

* Change code [Python] to Python

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: mariana <marianameireles@protonmail.com>
2022-09-23 14:14:00 +02:00
Ted Patrick
c236269d13 Update MAINTAINERS.md (#782) 2022-09-22 02:23:39 +05:30
Subramanian Mahadevan
8f658e6d85 fixed Makefile to always clean and recopy the examples build folder (located under examples) (#772) 2022-09-21 16:25:13 +02:00
Fábio Rosado
d203b60f44 Update contributing docs and remove lint from makefile (#779) 2022-09-21 16:22:12 +02:00
pre-commit-ci[bot]
a1a16aba74 [pre-commit.ci] pre-commit autoupdate (#780)
updates:
- [github.com/asottile/pyupgrade: v2.37.3 → v2.38.0](https://github.com/asottile/pyupgrade/compare/v2.37.3...v2.38.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-09-21 13:56:20 +02:00
Valerio Maggio
6ded003447 Fixed typo in logger message (#774) 2022-09-19 09:31:59 +02:00
Madhur Tandon
4841e29fc6 refactor py-config to use json (#754)
* refactor py-config to use toml

* switch from toml to json and add unit tests

* fix test for py-config

* fix integration test

* use flat structure for JSON

* allow merging configs

* replace arrays instead of concatenating them

* remove extra keys from inline config of integration test

* simplify array replacement logic

* allow config from src to be partial as well

* add comments to unit tests

* add unit test for setting config from both inline and src

* handle parse errors + validate config supplied

* separate functions for src and inline

* suggested improvements

* show error message in red on parser error

* fix eslint

* use resolveJsonModule as true

* use default config defined as a variable without import

* remove disable eslint comment

* remove import for covfefe.json as well

* metadata injection

* add support for schema + extra keys

* use schema_version
2022-09-16 02:07:00 +05:30
Jeff Glass
0b014eea56 Execute pys-on* events when triggered, not at load (#686)
* Execute pys-on* events when triggered, not at load

Mimicing the behavior of Javascripts 'onLoad' event, we should
not be executing the use code at page-load time, only when
the event is triggered.

* Update examples to new syntax

* Fix merge issue

* Await running event handler code

* Restore pys-on* events with original behavior, deprecation warning

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

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

* xfail toga example

* Add missing { (typo)

* Adjust callback chandling to make linter happy

* Change alpha to latest (#760)

* Don't create custom elements in main and fix various small issues on tests (#747)

* Create custom elements when the runtime finishes loading

* Remove xfails and fix repl integration test

* Fix commented ignore

* Address Antonio's comments

* Fix bad rebase

* Make ure to wait for repl to be in attached state before asserting content

* Move createCustomeElement up so it runs before we close the loader, xfail flaky d3 test

* Fix xfail

* [pre-commit.ci] pre-commit autoupdate (#762)

updates:
- [github.com/pre-commit/mirrors-eslint: v8.23.0 → v8.23.1](https://github.com/pre-commit/mirrors-eslint/compare/v8.23.0...v8.23.1)

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

* change documentation to point to latest instead of frozen alpha (#764)

* Toga example is xpass

* Correct 'xpass' to 'xfail' mark

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Peter W <34256109+pww217@users.noreply.github.com>
Co-authored-by: Fábio Rosado <fabiorosado@outlook.com>
2022-09-14 20:33:42 -05:00
Peter W
1c0be16f30 change documentation to point to latest instead of frozen alpha (#764) 2022-09-13 12:02:05 -04:00
pre-commit-ci[bot]
27ba8bea2f [pre-commit.ci] pre-commit autoupdate (#762)
updates:
- [github.com/pre-commit/mirrors-eslint: v8.23.0 → v8.23.1](https://github.com/pre-commit/mirrors-eslint/compare/v8.23.0...v8.23.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-09-13 10:34:24 -05:00
Fábio Rosado
c566977749 Don't create custom elements in main and fix various small issues on tests (#747)
* Create custom elements when the runtime finishes loading

* Remove xfails and fix repl integration test

* Fix commented ignore

* Address Antonio's comments

* Fix bad rebase

* Make ure to wait for repl to be in attached state before asserting content

* Move createCustomeElement up so it runs before we close the loader, xfail flaky d3 test

* Fix xfail
2022-09-13 16:59:33 +02:00
Peter W
5c1b785b4b Change alpha to latest (#760) 2022-09-12 12:07:44 -04:00
Jeff Glass
8657dfb5da Docs: How To Pass Objects between JavaScript and PyScript (#753)
* Begin writeup

* Draft full howto

* Correct code errors in writeup, swap sections

* Add introduction, clarification

* Add link to howto:js in index

* Copyedit, update examples to Pyodide 21

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

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

* Add toc tree context

* Use print instead of js console in python examples

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: mariana <marianameireles@protonmail.com>
2022-09-12 04:29:25 -04:00
Neil Stoker
dfa837754e Remove line numbers from example (#745)
As per #565 the current best fix is to simply remove the line numbers from the output
2022-09-12 04:28:19 -04:00
Ted Patrick
0a7df78770 Add space before/after secrets value (#757)
Co-authored-by: Ted Patrick <tpatrick@anaconda.com>
2022-09-08 07:54:33 -05:00
Mariana Meireles
066ecbe022 Add docs on how to contribute to docs (#720)
* Add docs on how to contribute to docs

* Reword some phrases
2022-09-08 07:38:31 -04:00
Ted Patrick
6c80db810f pyon-dragover to py-dragover (#755)
typo in events
2022-09-07 16:31:59 -05:00
Mariana Meireles
6023c413ab Update CONTRIBUTING.md (#728)
* Update CONTRIBUTING.md

Adding docs on how to make a pr

* Rewording so ideas are clearer
2022-09-07 03:05:20 +05:30
Peter W
7910d040b6 fix ci file sync (#752) 2022-09-06 19:30:01 +02:00
pre-commit-ci[bot]
5bd99f5224 [pre-commit.ci] pre-commit autoupdate (#750)
updates:
- [github.com/psf/black: 22.6.0 → 22.8.0](https://github.com/psf/black/compare/22.6.0...22.8.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-09-06 10:12:52 -05:00
Antonio Cuni
f3157b377f Improve JS logging (#743)
This PR tries to improve and rationalize what we log. Key points:
- introduce `logger.ts`: each file/component is encouraged to use the logger instead of writing directly to `console.*`
    * the logger automatically prepend a prefix like `[py-config]`, `[py-env]` which make it easier to understand where a certain message is printed from
    * it provide a central place where to add more features in the future. E.g., I can imagine having a config setting to completely silence the logs (not implemented yet)
- use the new loggers everywhere
- write to `.info()` instead of `.log()`. The idea is to keep `console.log` free, so that for the users it's easier to tell apart their own messages and the pyscript ones
- generally improve what we log. This is an endless exercise, but I tried to print more things which are useful to understand what's going on and in which order the various things are executed, and remove prints which were clearly debugging leftovers
2022-09-06 15:18:41 +02:00
Madhur Tandon
e31e03afde add tests for runtime config inside py-config and remove usage of indexURL (#734)
* add integration test for py-config

* fix bug

* fix test

* remove indexURL altogether

* make jest happy

* fix create_proxy import

* check that py-config loads an older version

* add unit test

* suggested changes

* don't use /tmp because of bandit
2022-09-01 01:02:43 +05:30
Peter W
eddde7c94c [WIP] Actions workflow cleanup/readability (#695)
* discrete steps and artifact passing

* fix small mistakes, prep test

* upload path

* fix path again

* change again

* checkout for test

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

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

* formatting

* artifact paths

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

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

* diagnostics

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

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

* simplify

* small cosemtic changes

* move upload to end

* needs:

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-08-31 10:01:41 -05:00
pre-commit-ci[bot]
7be72ee4c1 [pre-commit.ci] pre-commit autoupdate (#737)
updates:
- [github.com/pre-commit/mirrors-eslint: v8.22.0 → v8.23.0](https://github.com/pre-commit/mirrors-eslint/compare/v8.22.0...v8.23.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-08-30 19:49:15 +05:30
woxtu
6731467514 Update TypeScript to 4.8 (#739) 2022-08-30 19:47:14 +05:30
Madhur Tandon
8dd699d235 Remove workaround for sed and upgrade pyodide to 0.21.2 (#738)
* remove sed workaround + upgrade version

* upgrade package.json and package-lock.json
2022-08-30 18:19:00 +05:30
woxtu
6cb81b5c3d Remove the Svelte plugins for ESLint/Prettier (#733)
* Remove the Svelte plugins for Prettier

* Remove the Svelte plugins for ESLint
2022-08-29 01:13:16 +05:30
woxtu
17187ba3ec Remove the Svelte plugins for Rollup (#730) 2022-08-27 02:40:37 +05:30
Madhur Tandon
531ee928b0 fix linting and types related issues (#731) 2022-08-27 02:18:01 +05:30
Madhur Tandon
db806a5df9 make runtime an object of Runtime class when it is just an object (#729)
* make runtime an object of Runtime class when it is just an object

* fix constructor

* remove explicit string type

* define default values out of pyodide runtime and use default config instead of initializing a DEFAULT_RUNTIME instance

Co-authored-by: Fabio Pliger <fabio.pliger@gmail.com>
2022-08-26 15:19:03 -05:00
Fábio Rosado
9de154595a Add more unit tests for PyButton and BaseEvalElement (#711)
* Add more unit tests

* Rebase main and fix failing tests

* xfail flaky test

* Fix import
2022-08-26 08:25:51 -04:00
Fábio Rosado
9e4cb79679 Fix: Examples not importing pyscript from build folder when running locally (#705)
* Copy build to examples if not running prod

* Add rollup-copy as a dev dep

Co-authored-by: mariana <marianameireles@protonmail.com>
2022-08-25 18:05:17 -04:00
Bikarna Pokharel
b7834073b8 Remove unwanted code and/or text (#589)
* Remove unwanted class attribute

Multiple attributes with the same name are illegal in html. Nonetheless, the class task-check is not doing anything here.

* Remove to=task_list

The argument to=task_list is not playing any role, since the function works well without it.

Please see comment in pyscript.py

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

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

* Clean comment

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: mariana <marianameireles@protonmail.com>
2022-08-25 17:30:54 -04:00
woxtu
b0e56577b5 Fix misused promises (#723)
* Remove an unnecessary await

* Add an async IIFE wrapper
2022-08-26 02:17:29 +05:30
tkeech1
ccb0e6b269 585: Add a line to the makefile to run the examples on a python serve… (#689)
* 585: Add a line to the makefile to run the examples on a python server with one line command

* Build exs and run server with previous makefile cmds

Co-authored-by: Todd Keech <tkeech1@hotmail.com>

Co-authored-by: mariana <marianameireles@protonmail.com>
2022-08-25 16:43:25 -04:00
Madhur Tandon
edfd4baa1f import create_proxy and to_js from pyodide.ffi (#725) 2022-08-25 22:11:18 +02:00
woxtu
0f50f4a9fd Remove overwriting properties (#724) 2022-08-25 23:27:13 +05:30
Madhur Tandon
47494e62a7 Fix and enable integration tests in CI (#721)
* run jest only inside node + fix indexURL for integration tests

* add integration tests to CI

* use pytest from env
2022-08-25 22:01:05 +05:30
Fábio Rosado
7aa25712d9 Update tsconfig to use es2017 (#704) 2022-08-24 21:01:51 -05:00
Madhur Tandon
1db155570d PyodideRuntime should be one of the runtimes (#698)
* PyodideRuntime should be one of the runtimes

* subsume interpreter into runtime API

* fix eslint

* add comments

* move initializers, postInitializers, scriptsQueue, etc. to initialize() of Runtime Super Class

* modify comment for initialize

* small renaming

* change id to default

* fix pyscript.py import

* try adding tests

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

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

* Add inlineDynamicImports option

* Make jest happy about ESM modules

* Attempt to make jest happy about pyodide

* point to version in accordance with node module being used

* fix base.ts

* fix tests

* fix indexURL path determination

* edit pyodide.asm.js as a part of setup process

* load runtime beforeAll tests

* add test for loading a package

* use only runPythonAsync underneath for pyodide

* import PyodideInterface type directly from pyodide

* add some comments

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Philipp Rudiger <prudiger@anaconda.com>
2022-08-25 02:33:36 +05:30
Dylan
1054e8e644 Add clarification where to place script and stylesheet - README.md (#713)
* Update README.md

UPDATE: Specified to add files to head tag of HTML file

* UPDATE README.md

UPDATE README: Specify to add CSS and JS file to head tag of HTML

* Update README.md

Clean up wording.
2022-08-24 11:10:20 +05:30
Fábio Rosado
aa429f34d8 Add more integration tests for py-components and examples (#709)
* Add more integration tests for py-components and examples

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

* remove xfail mark since we merged fix for issue 707

Co-authored-by: Fabio Pliger <fabio.pliger@gmail.com>
2022-08-23 18:22:44 -05:00
Fábio Rosado
e351889811 Set importmaps to an empty array (#708)
Use const instead
2022-08-23 18:03:52 -05:00
Fabio Pliger
24a70a8273 fix error management bug and improve examples automation (#717)
* add build to the make examples cmd so that it automates more to the user

* temporarily improve error managament when executing eval until we properly refactor the whole io and error workflow

* add minimal test for format_mime
2022-08-23 17:12:30 -05:00
Fabio Pliger
3f26657116 update pyodide version (#719) 2022-08-23 17:11:42 -05:00
pre-commit-ci[bot]
d41669af8b [pre-commit.ci] pre-commit autoupdate (#710)
* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/codespell-project/codespell: v2.1.0 → v2.2.1](https://github.com/codespell-project/codespell/compare/v2.1.0...v2.2.1)

* push

* push

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: pww217 <pwilson@anaconda.com>
2022-08-23 17:07:04 -05:00
Peter W
56466c2a00 fix typo 2022-08-23 17:06:19 -05:00
Fabio Pliger
fa7a97ca30 Bootstrap python tests (#697)
* move current integration tests to the integration folder

* move pyscript.py into its own python folder

* change the path for python unit testing files

* change pyscript.py path

* Update Makefile

* remove echo

* replace conda run with pytest directly

* oops, add python test files I embarrassingly forgot to add

Co-authored-by: Peter W <34256109+pww217@users.noreply.github.com>
2022-08-18 16:59:07 -05:00
Fabio Pliger
8aba271a42 Add TS tests tooling (#661)
* install test dependencies

* change config for tests

* fix linter failing tests

* add basic test file

* add custom element registration to test

* update dependencies

* add jest config file

* fix test calls on makefile and minor fix on test

* update local npm version

* clean testm file
2022-08-16 16:39:42 -05:00
Philipp Rudiger
8275aa2810 Ensure that angle brackets in pyscript tag are escaped before parsing (#684)
* Ensure that angle brackets in pyscript tag are escaped before parsing

* Improve tests

* Update pyscriptjs/tests/test_01_basic.py

Co-authored-by: James A. Bednar <jbednar@users.noreply.github.com>

Co-authored-by: James A. Bednar <jbednar@users.noreply.github.com>
2022-08-16 21:41:09 +05:30
imbev
410ddf314c Correct typo in CONTRIBUTING.md (#692) 2022-08-16 10:12:09 -05:00
pre-commit-ci[bot]
fa217bee20 [pre-commit.ci] pre-commit autoupdate (#691)
updates:
- [github.com/pre-commit/mirrors-eslint: v8.21.0 → v8.22.0](https://github.com/pre-commit/mirrors-eslint/compare/v8.21.0...v8.22.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-08-16 09:56:13 -05:00
Antonio Cuni
817d0edc69 Use the new testing machinery for test_examples (#676)
* WIP: start to use the PyScriptTests machinery to test the examples

* factor test_hello_world out of test_examples

* B011 forbids 'assert False' in tests because python -O remove asserts. Thank you, I knew that.

* improve test_simple_clock and remove it from test_examples

* test_altair

* test_bokeh

* rename

* kill the parametrized test_example and write individual tests for each of them

* test_kmeans it's slow, increase the timeout

* improve these xfail

* kill wait_for_load, no longer needed

* write the name of the issue

* add issue number

* add a trick which I discovered to run test interactively

* move the docstring inside the class
2022-08-11 19:18:42 -05:00
Antonio Cuni
513dfe0b42 Introduce PyScriptTest, an helper class to write integration tests (#663)
This PR is about integration tests: they use playwright to load HTML pages in the browser and check that PyScript works as intended, as opposed to unit tests like the ones being introduced by #665 and #661.

The main goal of this PR is to introduce some machinery to make such tests easier to write, read and maintain, with some attention to capture enough information to produce useful error messages in case they fail in the CI.

In order to use the machinery, you need to subclass tests.support.PyScriptTest, which provides several useful API calls in the form self.xxx().

See the full description here:
https://github.com/pyscript/pyscript/pull/663

Co-authored-by: Mariana Meireles <marian.meireles@gmail.com>
Co-authored-by: mariana <marianameireles@protonmail.com>
2022-08-10 12:29:59 +02:00
pre-commit-ci[bot]
bd7a20309b [pre-commit.ci] pre-commit autoupdate (#669)
updates:
- [github.com/PyCQA/flake8: 5.0.2 → 5.0.4](https://github.com/PyCQA/flake8/compare/5.0.2...5.0.4)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-08-09 09:13:52 -05:00
woxtu
a726be3c7c Remove unnecessary await (#662) 2022-08-08 23:16:02 +05:30
Mariana Meireles
10f2054e9a Create command to run tests locally (#668) 2022-08-08 10:07:37 +02:00
Fabio Pliger
2fa47f310d remove output manager code injection in repl source (#664) 2022-08-05 18:50:20 +02:00
Antonio Cuni
5b927a70c2 PyItemTemplate.strike() uses the CSS class line-through, which was part of Tailwind. Reintroduce it in our custom CSS. This fixes the todo-pylist example (#638) 2022-08-03 12:29:39 +02:00
Peter W
2a59ff8e68 point examples to /latest instead of /alpha (#624)
* point examples to /unstable instead of /alpha

* Change makefile from alpha to unstable

* unstable -> latest

* Update Makefile

* Update repl.html

Co-authored-by: mariana <marianameireles@protonmail.com>
2022-08-02 13:59:50 -04:00
woxtu
e4d1befcdb Specify the array type (#653) 2022-08-02 21:10:08 +05:30
ic-768
844e04ff96 Minor refactoring (#557)
* checkpoint: added some text

* checkpoint: added setting up the environment, tips for writing good issues

* continuing to fill it out

* added more detailed description of the process of creating a change and some other cleanup.

* added a getting started section and cleaned up the grammar. This ready for the PR now

* forgot to add the new section to the TOC

* Changes

* Minor fixes

* Minor fixes

* Minor fixes

* Minor fixes

* Minor fixes

* Minor fixes

* Fix

Co-authored-by: Kevin Goldsmith <kgoldsmith@anaconda.com>
Co-authored-by: Matt Kramer <mkramer@anaconda.com>
2022-08-02 20:58:21 +05:30
pre-commit-ci[bot]
cc05a98b0e [pre-commit.ci] pre-commit autoupdate (#650)
updates:
- [github.com/PyCQA/flake8: 4.0.1 → 5.0.2](https://github.com/PyCQA/flake8/compare/4.0.1...5.0.2)
- [github.com/asottile/pyupgrade: v2.37.2 → v2.37.3](https://github.com/asottile/pyupgrade/compare/v2.37.2...v2.37.3)
- [github.com/pre-commit/mirrors-eslint: v8.20.0 → v8.21.0](https://github.com/pre-commit/mirrors-eslint/compare/v8.20.0...v8.21.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-08-02 10:27:46 -05:00
Madhur Tandon
006d161a32 cleanup leftovers such as mode and pyodideReadyPromise (#649)
* remove usage of mode

* clean up pyodideReadyPromise
2022-08-02 20:44:41 +05:30
Madhur Tandon
a4839db79a fix tests by changing serving dir (#652)
* fix tests by changing serving dir

* [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>
2022-08-02 20:35:09 +05:30
Madhur Tandon
87e3b5b1dc remove App.svelte as CSS is already present in pyscript_base.css (#648)
* remove App.svelte as CSS is already present in pyscript_base.css

* remove usage of app
2022-08-02 17:01:23 +02:00
Madhur Tandon
faa900d502 change args for Linux for sed (#651) 2022-08-02 12:27:56 +02:00
woxtu
a5275db3ec Specify the type of store value (#643) 2022-08-01 21:45:20 +05:30
Madhur Tandon
77e017a574 Add myself as maintainer as per discussion (#636) 2022-08-01 21:40:21 +05:30
Madhur Tandon
8ed8ddbf76 use pip (latest) to install playwright (#647) 2022-08-01 20:45:49 +05:30
Philipp Rudiger
eb31978488 Add tests verifying correct example behavior (#625) 2022-07-29 14:42:49 -04:00
woxtu
677d708588 Make the stores simple (#564) 2022-07-28 22:09:21 -04:00
Nicholas Tollervey
ade0dca8f9 Update to latest Pyodide (#631) 2022-07-28 20:52:54 -04:00
Fabio Pliger
8e1cd0b268 minor changes to make linting happier (#598)
* minor changes to make linting happy

* remove cast since linter wasn't happy

* address PR comments
2022-07-28 15:30:03 -05:00
woxtu
9102768366 Improve with string interpolations (#604)
* Use string interpolations

* Fix the key in py-env
2022-07-28 11:07:10 -04:00
pre-commit-ci[bot]
0c722b9164 [pre-commit.ci] pre-commit autoupdate (#630)
updates:
- [github.com/asottile/pyupgrade: v2.37.1 → v2.37.2](https://github.com/asottile/pyupgrade/compare/v2.37.1...v2.37.2)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-07-28 11:05:34 -04:00
takahiro ono
b6f514451a fix links in contribution & troubleshooting doc (#632) 2022-07-28 11:04:55 -04:00
Mariana Meireles
c49fdfc56c Re-introduce xPyWidget update on CustomWidget's class (#576)
Co-authored-by: mariana <marianameireles@protonmail.com>
2022-07-21 15:13:11 -05:00
Peter W
676e04b28e sync examples on stable releases (#623) 2022-07-21 16:00:02 -04:00
Antonio Cuni
6aa864a351 enable blank issues (#626) 2022-07-20 15:30:28 -04:00
pre-commit-ci[bot]
72acb4826c [pre-commit.ci] pre-commit autoupdate (#620)
updates:
- [github.com/pre-commit/mirrors-eslint: v8.19.0 → v8.20.0](https://github.com/pre-commit/mirrors-eslint/compare/v8.19.0...v8.20.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-07-19 17:02:10 -05:00
pre-commit-ci[bot]
734be5f355 [pre-commit.ci] pre-commit autoupdate (#594)
updates:
- [github.com/asottile/pyupgrade: v2.34.0 → v2.37.1](https://github.com/asottile/pyupgrade/compare/v2.34.0...v2.37.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-07-18 11:43:38 -04:00
Leonardo Pliger
cede06ae19 Clearer contribution docs & add troubleshooting page (#597)
* Expanded upon the make setup docs

* Fixed formatting & speling, added troubleshooting

* same

* [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>
2022-07-18 11:42:32 -04:00
Christian Clauss
19491d8010 f-string accelerates examples.utils.format_date() (#591) 2022-07-12 11:26:47 -05:00
pww217
c580aac991 add workflow dispatch to unstable 2022-07-07 14:47:23 -05:00
pww217
032d1aaad7 change path to /build 2022-07-07 14:45:33 -05:00
Mariana Meireles
afa216dc5e Mv build folder from examples/build to build/ (#571)
Co-authored-by: mariana <marianameireles@protonmail.com>
2022-07-07 11:29:09 -03:00
Nicholas Tollervey
69339fe3de Add myself to the list of maintainers as per recent discussion. (#573) 2022-07-06 10:36:23 +02:00
pre-commit-ci[bot]
571bb2b294 [pre-commit.ci] pre-commit autoupdate (#570)
updates:
- [github.com/psf/black: 22.3.0 → 22.6.0](https://github.com/psf/black/compare/22.3.0...22.6.0)
- [github.com/macisamuele/language-formatters-pre-commit-hooks: v2.3.0 → v2.4.0](https://github.com/macisamuele/language-formatters-pre-commit-hooks/compare/v2.3.0...v2.4.0)
- [github.com/pre-commit/mirrors-eslint: v8.18.0 → v8.19.0](https://github.com/pre-commit/mirrors-eslint/compare/v8.18.0...v8.19.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-07-04 21:03:35 -03:00
Fabio Pliger
91a09a09f7 Rename examples cmd and add checks (#563)
* add checks for node and npm versions

* rename example cmd to examples
2022-07-04 15:21:17 -03:00
munyoudoum
9b3433f6ae fix: typos in play_mario, handtrack examples (#566) 2022-07-04 15:19:18 -03:00
Leonardo Pliger
ee9b0960f7 Add support to py- events that map all js events available (#561)
* added the  py- events throughout pyscript.ts

* Integrated the py- event throughout the examples

* Fixed spelling error
2022-06-29 19:49:21 -03:00
woxtu
506ac2574f Add type annotations (#562) 2022-06-28 14:17:23 -05:00
ic-768
dc84d7c1b5 Minor improvements (#556)
* checkpoint: added some text

* checkpoint: added setting up the environment, tips for writing good issues

* continuing to fill it out

* added more detailed description of the process of creating a change and some other cleanup.

* added a getting started section and cleaned up the grammar. This ready for the PR now

* forgot to add the new section to the TOC

* Minor fixes

Co-authored-by: Kevin Goldsmith <kgoldsmith@anaconda.com>
Co-authored-by: Matt Kramer <mkramer@anaconda.com>
2022-06-28 14:16:05 -05:00
Fabio Pliger
fcaa57307f See you later tailwind (#452)
* start removing tailwind and rebuilding some css

* add css to pybox and add class to repl

* set output component visibility

* replace tailwind class with single component class

* add styles to css

* replace classes on  button

* replace classes on input

* replace classes in title

* replace classes on list

* replace classes

* add new style file

* add list element style

* remove tailwind classes from todo example

* revert link on examples files

* remove tailwind config files

* remove commented old code

* add missing ;
2022-06-24 18:30:07 -05:00
Mariana Meireles
d25e754beb Move dashboard workflow to worflow dir (#554)
Co-authored-by: mariana <marianameireles@protonmail.com>
2022-06-24 17:01:52 -05:00
Peter W
7f6f411ea8 Try new method to keep CI from running on forks (#538) 2022-06-24 16:55:15 -05:00
Fabio Pliger
96a73e31f3 add diataxis info to docs readme (#548)
* add loca build info to htmlserve

* add info about diataxis to the docs/readme

* add diataxis img
2022-06-24 17:06:47 -03:00
woxtu
c7942d7d8f Use the type definition of pyodide (#535)
* Import the type definition

* Add type annotations
2022-06-24 14:25:14 -05:00
Leonardo Pliger
479348eec9 Added section and cleared up documentation in getting started (#547)
* add section and cleared up documentation

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

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

* I fixed the typo in getting-started

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-06-24 16:14:48 -03:00
Peter W
ebfed27630 release, latest, unstable branches (#550)
* build unstable workflow

* releases are synced to /latest and /releases/YYYY.MM.MICRO/

* slashes
2022-06-24 10:32:00 -05:00
Mariana Meireles
8923485169 Update feature-request.yml (#541)
Fix typo
2022-06-23 17:44:55 -05:00
Brandon High
d62de26683 Fix inconsistencies and reduce verbosity in examples/index.html (#545) 2022-06-23 17:40:36 -05:00
pww217
1dd9c5b009 small renaming 2022-06-23 11:22:33 -05:00
128 changed files with 15149 additions and 2140 deletions

View File

@@ -1,4 +1,4 @@
blank_issues_enabled: false blank_issues_enabled: true
contact_links: contact_links:
- name: Question - name: Question
url: https://community.anaconda.cloud/c/tech-topics/pyscript url: https://community.anaconda.cloud/c/tech-topics/pyscript

View File

@@ -35,7 +35,7 @@ body:
required: true required: true
- label: I searched for other feature requests and couldn't find a duplicate (including also the ``type-feature`` tag) - label: I searched for other feature requests and couldn't find a duplicate (including also the ``type-feature`` tag)
required: true required: true
- label: I confirmed that it's not related to another project are area (see the above section) - label: I confirmed that it's not related to another project area (see the above section)
required: true required: true
- type: textarea - type: textarea
id: request-idea id: request-idea

View File

@@ -1,4 +1,4 @@
name: '[CI] Build Latest' name: '[CI] Build Unstable'
on: on:
push: # Only run on merges into main that modify files under pyscriptjs/ push: # Only run on merges into main that modify files under pyscriptjs/
@@ -8,27 +8,23 @@ on:
- pyscriptjs/** - pyscriptjs/**
- .github/workflows/build-latest.yml # Test that workflow works when changed - .github/workflows/build-latest.yml # Test that workflow works when changed
pull_request: # Run on any PR that modifies files in pyscriptjs/ pull_request: # Run on any PR that modifies files under pyscriptjs/
branches: branches:
- main - main
paths: paths:
- pyscriptjs/** - pyscriptjs/**
- .github/workflows/build-latest.yml # Test that workflow works when changed - .github/workflows/build-unstable.yml # Test that workflow works when changed
workflow_dispatch:
env:
MINICONDA_PYTHON_VERSION: py38
MINICONDA_VERSION: 4.11.0
jobs:
BuildAndTest:
runs-on: ubuntu-latest
defaults: defaults:
run: run:
working-directory: pyscriptjs working-directory: pyscriptjs
env:
jobs: MINICONDA_PYTHON_VERSION: py38
build: MINICONDA_VERSION: 4.11.0
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps: steps:
- name: Checkout - name: Checkout
@@ -58,17 +54,46 @@ jobs:
- name: Setup Environment - name: Setup Environment
run: make setup run: make setup
- name: Build and Test - name: Build
run: make test run: make build
- name: TypeScript Tests
run: make test-ts
- name: Python Tests
run: make test-py
- name: Integration Tests
run: make test-integration
- uses: actions/upload-artifact@v3
with:
name: pyscript
path: |
pyscriptjs/build/
if-no-files-found: error
retention-days: 7
Deploy:
runs-on: ubuntu-latest
needs: BuildAndTest
if: github.ref == 'refs/heads/main' # Only deploy on merge into main
permissions:
contents: read
id-token: write
steps:
- uses: actions/download-artifact@v3
with:
name: pyscript
path: ./build/
# Deploy to S3 # Deploy to S3
- name: Configure AWS credentials - name: Configure AWS credentials
if: github.ref == 'refs/heads/main' # Only deploy on merge into main
uses: aws-actions/configure-aws-credentials@v1.6.1 uses: aws-actions/configure-aws-credentials@v1.6.1
with: with:
aws-region: ${{ secrets.AWS_REGION }} aws-region: ${{ secrets.AWS_REGION }}
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }} role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}
- name: Sync to S3 - name: Sync to S3
if: github.ref == 'refs/heads/main' run: aws s3 sync --quiet ./build/ s3://pyscript.net/unstable/
run: aws s3 sync --quiet ./examples/build/ s3://pyscript.net/latest/

View File

@@ -18,8 +18,7 @@ concurrency:
jobs: jobs:
build: build:
if: >- if: github.repository_owner == 'pyscript'
!github.event.repository.fork
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: read

View File

@@ -1,4 +1,4 @@
name: '[CI] Build Release' name: '[CI] Prepare Release'
on: on:
push: push:

View File

@@ -57,5 +57,6 @@ jobs:
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }} role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}
- name: Sync to S3 - name: Sync to S3
run: | # Overwrite "latest" alpha + versioned subdirectory run: | # Update /latest and create an explicitly versioned directory under releases/YYYY.MM.MICRO/
aws s3 sync --quiet ./examples/build/ s3://pyscript.net/releases/${{ github.ref_name }} aws s3 sync --quiet ./build/ s3://pyscript.net/latest/
aws s3 sync --quiet ./build/ s3://pyscript.net/releases/${{ github.ref_name }}/

26
.github/workflows/publish-snapshot.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: '[CI] Publish Snapshot'
# Copy /unstable/ to /snapshots/2022.09.1.RC1/
on:
workflow_dispatch:
inputs:
snapshot_version:
description: 'The calver version of this snapshot: 2022.09.1 or 2022.09.1.RC1'
type: string
required: true
jobs:
snapshot:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1.6.1
with:
aws-region: ${{ secrets.AWS_REGION }}
role-to-assume: ${{ secrets.AWS_OIDC_RUNNER_ROLE }}
- name: Sync to S3
run: >
aws s3 sync s3://pyscript.net/unstable/ s3://pyscript.net/snapshots/${{ inputs.snapshot_version }}/

View File

@@ -1,12 +1,8 @@
name: '[CI] Sync Examples' name: '[CI] Sync Examples'
on: on:
push: # Only run on merges into main that modify files under examples/ release:
branches: types: [published]
- main
paths:
- examples/**
- .github/workflows/sync-examples.yml # Test that workflow works when changed
jobs: jobs:
build: build:

2
.gitignore vendored
View File

@@ -136,3 +136,5 @@ dmypy.json
.pyre/ .pyre/
node_modules/ node_modules/
coverage/

View File

@@ -25,20 +25,21 @@ repos:
hooks: hooks:
- id: bandit - id: bandit
args: args:
- --skip=B201 - --skip=B101,B201
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 22.3.0 rev: 22.8.0
hooks: hooks:
- id: black - id: black
- repo: https://github.com/codespell-project/codespell - repo: https://github.com/codespell-project/codespell
rev: v2.1.0 rev: v2.2.1
hooks: hooks:
- id: codespell # See 'setup.cfg' for args - id: codespell # See 'setup.cfg' for args
- repo: https://github.com/PyCQA/flake8 - repo: https://github.com/PyCQA/flake8
rev: 4.0.1 rev: 5.0.4
hooks: hooks:
- id: flake8 # See 'setup.cfg' for args - id: flake8 # See 'setup.cfg' for args
additional_dependencies: [flake8-bugbear, flake8-comprehensions] additional_dependencies: [flake8-bugbear, flake8-comprehensions]
@@ -51,21 +52,21 @@ repos:
args: [--profile, black] args: [--profile, black]
- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
rev: v2.3.0 rev: v2.4.0
hooks: hooks:
- id: pretty-format-yaml - id: pretty-format-yaml
args: [--autofix, --indent, '4'] args: [--autofix, --indent, '4']
exclude: .github/ISSUE_TEMPLATE/.*\.yml$ exclude: .github/ISSUE_TEMPLATE/.*\.yml$
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v2.34.0 rev: v2.38.2
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: args:
- --py310-plus - --py310-plus
- repo: https://github.com/pre-commit/mirrors-eslint - repo: https://github.com/pre-commit/mirrors-eslint
rev: v8.18.0 rev: v8.24.0
hooks: hooks:
- id: eslint - id: eslint
files: pyscriptjs/src/.*\.[jt]sx?$ # *.js, *.jsx, *.ts and *.tsx files: pyscriptjs/src/.*\.[jt]sx?$ # *.js, *.jsx, *.ts and *.tsx

View File

@@ -9,58 +9,182 @@ Thank you for wanting to contribute to the PyScript project!
* [Reporting bugs](#reporting-bugs) * [Reporting bugs](#reporting-bugs)
* [Reporting security issues](#reporting-security-issues) * [Reporting security issues](#reporting-security-issues)
* [Asking questions](#asking-questions) * [Asking questions](#asking-questions)
* [Setting up your environment](#setting-up-your-environment) * [Setting up your local environment](#setting-up-your-local-environment)
* [Places to start](#places-to-start) * [Places to start](#places-to-start)
* [Submitting a change](#submitting-a-change) * [Submitting a change](#submitting-a-change)
* [License terms for contributions](#license-terms-for-contributions) * [License terms for contributions](#license-terms-for-contributions)
* [Becoming a maintainer](#becoming-a-maintainer) * [Becoming a maintainer](#becoming-a-maintainer)
* [Trademarks](#trademarks) * [Trademarks](#trademarks)
## Code of Conduct # Code of Conduct
The [PyScript Code of Conduct](https://github.com/pyscript/governance/blob/main/CODE-OF-CONDUCT.md) governs the project and everyone participating in it. By participating, you are expected to uphold this code. Please report unacceptable behavior to the maintainers or administrators as described in that document. The [PyScript Code of Conduct](https://github.com/pyscript/governance/blob/main/CODE-OF-CONDUCT.md) governs the project and everyone participating in it. By participating, you are expected to uphold this code. Please report unacceptable behavior to the maintainers or administrators as described in that document.
## Contributing # Contributing
### Reporting bugs ## Reporting bugs
Bugs are tracked on the [project issues page](https://github.com/pyscript/pyscript/issues). Please check if your issue has already been filed by someone else by searching the existing issues before filing a new one. Once your issue is filed, it will be triaged by another contributor or maintainer. If there are questions raised about your issue, please respond promptly. Bugs are tracked on the [project issues page](https://github.com/pyscript/pyscript/issues). Please check if your issue has already been filed by someone else by searching the existing issues before filing a new one. Once your issue is filed, it will be triaged by another contributor or maintainer. If there are questions raised about your issue, please respond promptly.
#### Creating useful issues ## Creating useful issues
* Use a clear and descriptive title. * Use a clear and descriptive title.
* Describe the specific steps that reproduce the problem with as many details as possible so that someone can verify the issue. * Describe the specific steps that reproduce the problem with as many details as possible so that someone can verify the issue.
* Describe the behavior you observed, and the behavior you had expected. * Describe the behavior you observed, and the behavior you had expected.
* Include screenshots if they help make the issue clear. * Include screenshots if they help make the issue clear.
### Reporting security issues ## Reporting security issues
If you aren't confident that it is appropriate to submit a security issue using the above process, you can e-mail it to security@pyscript.net If you aren't confident that it is appropriate to submit a security issue using the above process, you can e-mail it to security@pyscript.net
### Asking questions ## Asking questions
If you have questions about the project, using PyScript, or anything else, please ask in the [PyScript forum](https://community.anaconda.cloud/c/tech-topics/pyscript). If you have questions about the project, using PyScript, or anything else, please ask in the [PyScript forum](https://community.anaconda.cloud/c/tech-topics/pyscript).
### Setting up your environment ## Setting up your local environment
* Fork the repository - [quicklink](https://github.com/pyscript/pyscript/fork)
* Clone your fork of the project
* clone the repo
``` ```
git clone https://github.com/pyscript/pyscript git clone https://github.com/<your username>/pyscript
``` ```
* cd into the main project folder
* Add the original project as your upstream (this will allow you to pull the latest changes)
```
git remote add upstream git@github.com:pyscript/pyscript.git
```
* cd into the `pyscriptjs` folder using the line below in your terminal (if your terminal is already in pyscript then use **cd pyscriptjs** instead)
``` ```
cd pyscript/pyscriptjs cd pyscript/pyscriptjs
``` ```
* install the dependencies with npm install - make sure to use nodejs version >= 16 * Install the dependencies with the command below
``` ```
npm install make setup
``` ```
* run npm run dev to build and run the dev server. This will also watch for changes and rebuild when a file is saved. &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; **NOTE**: If `make setup` gives a node/npm version required error then go to [troubleshooting](https://github.com/pyscript/pyscript/blob/main/TROUBLESHOOTING.md)
* You can also run the examples locally by running the command below in your terminal
```
make examples
```
* Run ***npm run dev*** to build and run the dev server. This will also watch for changes and rebuild when a file is saved.
``` ```
npm run dev npm run dev
``` ```
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; **NOTE**: To access your local build paste `http://localhost:8080` into your browser
### Places to start
Now that node and npm have both been updated `make setup` should work, and you can continue [setting up your local environment](#setting-up-your-local-environment) without problems (hopefully).
### Developing
* First, make sure you are using the latest version of the pyscript main branch
```
git pull upstream main
```
* Update your fork with the latest changes
```
git push origin main
```
* Activate the conda environment (this environment will contain all the necessary dependencies)
```
conda activate pyscriptjs/env/
```
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; **NOTE**: We are assuming you are in the root folder. If you are in the pyscriptjs you can run `conda activate env/` instead.
* Install pre-commit (you only need to do this once)
```
pre-commit install
```
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; **NOTE**: On first run, pre-commit installs a bunch of hooks that will be run when you commit changes to your branch - this will make sure that your code is following our style (it will also lint your code automatically).
* Create a branch for the issue that you want to work on
```
git checkout -b <your branch name>
```
* Work on your change
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; **NOTE**: If you are working on a python file, you may encounter linting issues when pre-commit runs. Pyscript uses [black](https://black.readthedocs.io/en/stable/) to fix any linting problems automatically. All you need to do is add the changes again and commit using your previous commit message (the previous one that failed didn't complete due to black formatting files).
* Run tests before pushing the changes
```
make tests
```
* When you make changes locally, double check that your contribution follows the PyScript formatting rules by running `npm run lint`. Note that in this case you're looking for the errors, <u>**NOT**</u> the warnings (Unless the warning is created by a local change). If an error is found by lint you should fix it <u>**before**</u> creating a pull request
#### Rebasing changes
Sometimes you might be asked to rebase main into your branch. You can do such by:
* Checkout into your main branch and pull the upstream changes
```
git checkout main
git pull upstream main
```
* Checkout your branch and rebase on main
```
git rebase -i main
```
If you have conflicts, you must fix them by comparing yours and incoming changes. Your editor can probably help you with this, but do ask for help if you need it!
* Once all conflicts have been fixed
```
git rebase --continue
```
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; **NOTE**: You may see more conflicts that you need to address until all are resolved.
* Force push the fixed conflicts
```
git push -f origin <your branch name>
```
### Building the docs
To build the documentation locally first make sure you are in the `docs` directory.
You'll need `make` and `conda` installed in your machine. The rest of the environment should be automatically download and created for you once you use the command:
```
make setup
```
Use `conda activate $environment_name` to activate your environment.
To add new information to the documentation make sure you conform with PyScript's code of conduct and with the general principles of Diataxis. Don't worry about reading too much on it, just do your best to keep your contributions on the correct axis.
Write your documentation files using [Markedly Structured Text](https://myst-parser.readthedocs.io/en/latest/syntax/optional.html), which is very similar to vanilla Markdown but with some addons to create the documentation infrastructure.
Once done, initialize a server to check your work:
```
make livehtml
```
Visible here: http:///127.0.0.1:8000
## Places to start
If you would like to contribute to PyScript, but you aren't sure where to begin, here are some suggestions. If you would like to contribute to PyScript, but you aren't sure where to begin, here are some suggestions.
@@ -69,15 +193,14 @@ If you would like to contribute to PyScript, but you aren't sure where to begin,
* **Look over the open pull requests.** Do you have comments or suggestions for the proposed changes? Add them. * **Look over the open pull requests.** Do you have comments or suggestions for the proposed changes? Add them.
* **Check out the examples.** Is there a use case that would be good to have sample code for? Create an example for it. * **Check out the examples.** Is there a use case that would be good to have sample code for? Create an example for it.
### Submitting a change # Submitting a change
All contributions must be licensed Apache 2.0, and all files must have a copy of the boilerplate license comment (can be copied from an existing file). All contributions must be licensed Apache 2.0, and all files must have a copy of the boilerplate license comment (can be copied from an existing file).
To create a change for PyScript, you can follow the process described [here](https://docs.github.com/en/get-started/quickstart/contributing-to-projects). To create a change for PyScript, you can follow the process described [here](https://docs.github.com/en/get-started/quickstart/contributing-to-projects).
* Fork a personal copy of the PyScript project. * Follow the steps in [setting your local environment](#setting-up-your-local-environment) and [developing](#developing)
* Make the changes you would like (don't forget to test them!) * Make the changes you would like (don't forget to test them with `make test`)
* Please squash all commits for a change into a single commit (this can be done using "git rebase -i"). Do your best to have a well-formed commit message for the change.
* Open a pull request back to the PyScript project and address any comments/questions from the maintainers and other contributors. * Open a pull request back to the PyScript project and address any comments/questions from the maintainers and other contributors.
## License terms for contributions ## License terms for contributions

View File

@@ -3,13 +3,17 @@
This document lists the Maintainers of the Project. Maintainers may be added once approved by the existing maintainers as described in the [Governance document](https://github.com/pyscript/pyscript/blob/main/GOVERNANCE.md). By adding your name to this list you are agreeing to abide by the Project governance documents and to abide by all of the Organization's polices, including the [code of conduct](https://github.com/pyscript/governance/blob/main/CODE-OF-CONDUCT.md), [trademark policy](https://github.com/pyscript/governance/blob/main/TRADEMARKS.md), and [antitrust policy](https://github.com/pyscript/governance/blob/main/TRADEMARKS.md). If you are participating because of your affiliation with another organization (designated below), you represent that you have the authority to bind that organization to these policies. This document lists the Maintainers of the Project. Maintainers may be added once approved by the existing maintainers as described in the [Governance document](https://github.com/pyscript/pyscript/blob/main/GOVERNANCE.md). By adding your name to this list you are agreeing to abide by the Project governance documents and to abide by all of the Organization's polices, including the [code of conduct](https://github.com/pyscript/governance/blob/main/CODE-OF-CONDUCT.md), [trademark policy](https://github.com/pyscript/governance/blob/main/TRADEMARKS.md), and [antitrust policy](https://github.com/pyscript/governance/blob/main/TRADEMARKS.md). If you are participating because of your affiliation with another organization (designated below), you represent that you have the authority to bind that organization to these policies.
| **NAME** | **Organization** | | **NAME** | **Organization** |
| ---------------- | ---------------- | | -------------------- | ---------------- |
| Fabio Pliger | Anaconda, Inc | | Fabio Pliger | Anaconda, Inc |
| Antonio Cuni | Anaconda, Inc | | Antonio Cuni | Anaconda, Inc |
| Philipp Rudiger | Anaconda, Inc | | Philipp Rudiger | Anaconda, Inc |
| Peter Wang | Anaconda, Inc | | Peter Wang | Anaconda, Inc |
| Kevin Goldsmith | Anaconda, Inc | | Kevin Goldsmith | Anaconda, Inc |
| Mariana Meireles | Anaconda, Inc | | Mariana Meireles | Anaconda, Inc |
| Nicholas H.Tollervey | Anaconda, Inc |
| Madhur Tandon | Anaconda, Inc |
| Ted Patrick | Anaconda, Inc |
| Jeff Glass | --- |
| --- | --- | | --- | --- |
______________________________________________________________________ ______________________________________________________________________

View File

@@ -4,7 +4,7 @@
### Summary ### Summary
PyScript is a Pythonic alternative to Scratch, JSFiddle, and other "easy to use" programming frameworks, with the goal of making the web a friendly, hackable place where anyone can author interesting and interactive applications. PyScript is a framework that allows users to create rich Python applications in the browser using HTML's interface and the power of [Pyodide](https://pyodide.org/en/stable/), [WASM](https://webassembly.org/), and modern web technologies.
To get started see the [getting started tutorial](docs/tutorials/getting-started.md). To get started see the [getting started tutorial](docs/tutorials/getting-started.md).
@@ -15,10 +15,12 @@ PyScript is a meta project that aims to combine multiple open technologies into
## Try PyScript ## Try PyScript
To try PyScript, import the appropriate pyscript files to your html page with: To try PyScript, import the appropriate pyscript files into the ```<head>``` tag of your html page with:
```html ```html
<link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" /> <head>
<script defer src="https://pyscript.net/alpha/pyscript.js"></script> <link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/latest/pyscript.js"></script>
</head>
``` ```
You can then use PyScript components in your html page. PyScript currently implements the following elements: You can then use PyScript components in your html page. PyScript currently implements the following elements:

18
TROUBLESHOOTING.md Normal file
View File

@@ -0,0 +1,18 @@
# Troubleshooting
This page is meant for troubleshooting common problems with PyScript.
## Table of contents:
* [Make Setup](#make-setup)
## Make setup
A lot of problems related to `make setup` are related to node and npm being outdated. Once npm and node are updated, `make setup` should work. You can follow the steps on the [npm documentation](https://docs.npmjs.com/try-the-latest-stable-version-of-npm) to update npm (the update command for Linux should work for Mac as well). Once npm has been updated you can continue to the instructions to update node below.
To update Node run the following commands in order (Most likely you'll be prompted for your user password, this is normal):
```
sudo npm cache clean -f
sudo npm install -g n
sudo n stable
```

View File

@@ -31,7 +31,8 @@ shell:
@echo 'conda activate $(env)' @echo 'conda activate $(env)'
htmlserve: htmlserve:
python -m http.server -d "$(BUILDDIR)/html/" @echo 'visit docs at http://localhost:8080'
python -m http.server -d "$(BUILDDIR)/html/" 8080
livehtml: livehtml:
sphinx-autobuild "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) sphinx-autobuild "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

View File

@@ -9,9 +9,23 @@ Before you start contributing to the documentation, it's worthwhile to
take a look at the general contributing guidelines for the PyScript project. You can find these guidelines here take a look at the general contributing guidelines for the PyScript project. You can find these guidelines here
[Contributing Guidelines](https://github.com/pyscript/pyscript/blob/main/CONTRIBUTING.md) [Contributing Guidelines](https://github.com/pyscript/pyscript/blob/main/CONTRIBUTING.md)
## Documentation Principles
The PyScript documentation is based on a documentation framework called [Diátaxis](https://diataxis.fr/). This framework helps to solve the problem of structure in technical documentation and identifies four modes of documentation - **tutorials, how-to guides, technical reference and explanation**. Each one of these modes answers to a different user need, fulfills a different purpose and requires a different approach to its creation.
The picture below gives a good visual representation of that separation of concerns:
![pyodide-pyscript](./img/diataxis.png)
So, please keep that in mind when contributing to the project documentation. For more information on, make sure to check [their website](https://diataxis.fr/).
### Setup ### Setup
The `docs` directory in the pyscript repository contains a The `docs` directory in the pyscript repository contains a
[Sphinx](https://www.sphinx-doc.org/) documentation project. Sphinx is a system [Sphinx](https://www.sphinx-doc.org/) documentation project. Sphinx is a system
that takes plaintext files containing documentation written in Markdown, along with that takes plaintext files containing documentation written in Markdown, along with
and static files like templates and themes, to build the static end result. static files like templates and themes, to build the static end result.
### Build
To learn how to build the docs, head over the [CONTRIBUTING](../CONTRIBUTING.md) page.

View File

@@ -1,7 +1,7 @@
<html> <html>
<head> <head>
<link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" /> <link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/alpha/pyscript.js"></script> <script defer src="https://pyscript.net/latest/pyscript.js"></script>
<style> <style>
.pulse { .pulse {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;

View File

@@ -19,7 +19,6 @@ To try it in your browser, copy the code below into an online HTML editor like W
```{literalinclude} ../_static/examples/what-is-pyscript.html ```{literalinclude} ../_static/examples/what-is-pyscript.html
--- ---
linenos:
``` ```
::: :::

View File

@@ -0,0 +1,210 @@
# How to make HTTP requests using `PyScript`, in pure Python
[Pyodide](https://pyodide.org), the runtime that underlies `PyScript`, does not have the `requests` module
(or other similar modules) available by default, which are traditionally used to make HTTP requests in Python.
However, it is possible to make HTTP requests in Pyodide using the modern `JavaScript` `fetch` API
([docs](https://developer.mozilla.org/en-US/docs/Web/API/fetch)). This example shows how to make common HTTP request
(GET, POST, PUT, DELETE) to an API, using only Python code! We will use asynchronous functions with
async/await syntax, as concurrent code is preferred for HTTP requests.
The purpose of this guide is not to teach the basics of HTTP requests, but to show how to make them
from `PyScript` using Python, since currently, the common tools such as `requests` and `httpx` are not available.
## Fetch
The `fetch` API is a modern way to make HTTP requests. It is available in all modern browsers, and in Pyodide.
Although there are two ways to use `fetch`, 1) using `JavaScript` from `PyScript`, and 2) using Pyodide's Python wrapper,
`Pyodide.http.pyfetch`, this example will only show how to use the Python wrapper. Still, the
[fetch documentation](https://developer.mozilla.org/en-US/docs/Web/API/fetch#parameters) is a useful reference, as its
parameters can be called from Python using the `pyfetch` wrapper.
## Pyodide.http, pyfetch, and FetchResponse
[Pyodide.http module](https://pyodide.org/en/stable/usage/api/python-api/http.html#module-pyodide.http) is a Python API
for dealing with HTTP requests. It provides the `pyfetch` function as a wrapper for the `fetch` API,
which returns a `FetchResponse` object whenever a request is made. Extra keyword arguments can be passed to `pyfetch`
which will be passed to the `fetch` API.
The returned object `FetchResponse` has familiar methods and properties
for dealing with the response, such as `json()` or `status`. See the
[FetchResponse documentation](https://pyodide.org/en/stable/usage/api/python-api/http.html#pyodide.http.FetchResponse)
for more information.
# Example
We will make async HTTP requests to [JSONPlaceholder](https://jsonplaceholder.typicode.com/)'s fake API using `pyfetch`.
First we write a helper function in pure Python that makes a request and returns the response. This function
makes it easier to make specific types of requests with the most common parameters.
## Python convenience function
```python
from pyodide.http import pyfetch, FetchResponse
from typing import Optional, Any
async def request(url: str, method: str = "GET", body: Optional[str] = None,
headers: Optional[dict[str, str]] = None, **fetch_kwargs: Any) -> FetchResponse:
"""
Async request function. Pass in Method and make sure to await!
Parameters:
url: str = URL to make request to
method: str = {"GET", "POST", "PUT", "DELETE"} from `JavaScript` global fetch())
body: str = body as json string. Example, body=json.dumps(my_dict)
headers: dict[str, str] = header as dict, will be converted to string...
Example, headers=json.dumps({"Content-Type": "application/json"})
fetch_kwargs: Any = any other keyword arguments to pass to `pyfetch` (will be passed to `fetch`)
Return:
response: pyodide.http.FetchResponse = use with .status or await.json(), etc.
"""
kwargs = {"method": method, "mode": "cors"} # CORS: https://en.wikipedia.org/wiki/Cross-origin_resource_sharing
if body and method not in ["GET", "HEAD"]:
kwargs["body"] = body
if headers:
kwargs["headers"] = headers
kwargs.update(fetch_kwargs)
response = await pyfetch(url, **kwargs)
return response
```
This function is a wrapper for `pyfetch`, which is a wrapper for the `fetch` API. It is a coroutine function,
so it must be awaited. It also has type hints, which are not required, but are useful for IDEs and other tools.
The basic idea is that the `PyScript` will import and call this function, then await the response. Therefore,
the script containing this function must be importable by `PyScript`.
For this example, we will name the file containing the Python code `request.py` and place it in the same directory as the file
containing the html code, which is described below.
## `PyScript` HTML code
In this How-to, the HTML code is split into separate code blocks to enable context highlighting (coloring of the Python
code inside the html code block), but in reality it is all in the same file. The first part is a bare bones `PyScript`
html page, using the [community examples](https://github.com/pyscript/pyscript-collective/) set-up. The second part is
the actual Python code for HTTP requests, which is wrapped in `<py-script>` tags, while the third block has the
concluding html code.
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>GET, POST, PUT, DELETE example</title>
<link rel="icon" type="image/png" href="favicon.png" />
<link rel="stylesheet" href="../build/pyscript.css" />
<script defer src="../build/pyscript.js"></script>
<py-env>
- paths:
- /request.py
</py-env>
</head>
<body><p>
Hello world request example! <br>
Here is the output of your request:
</p>
<py-script>
```
```python
import asyncio # important!!
import json
from request import request # import our request function.
baseurl = "https://jsonplaceholder.typicode.com/"
# GET
headers = {"Content-type": "application/json"}
response = await request(baseurl+"posts/2", method="GET", headers=headers)
print(f"GET request=> status:{response.status}, json:{await response.json()}")
# POST
body = json.dumps({"title": "test_title", "body": "test body", "userId": 1})
new_post = await request(baseurl+"posts", body=body, method="POST", headers=headers)
print(f"POST request=> status:{new_post.status}, json:{await new_post.json()}")
# PUT
body = json.dumps({"id": 1, "title": "test_title", "body": "test body", "userId": 2})
new_post = await request(baseurl+"posts/1", body=body, method="PUT", headers=headers)
print(f"PUT request=> status:{new_post.status}, json:{await new_post.json()}")
# DELETE
new_post = await request(baseurl+"posts/1", method="DELETE", headers=headers)
print(f"DELETE request=> status:{new_post.status}, json:{await new_post.json()}")
```
```html
</py-script>
<div>
<p>
You can also use other methods. See fetch documentation: <br>
https://developer.mozilla.org/en-US/docs/Web/API/fetch#parameters
</p>
</div>
<div>
<p>
See pyodide documentation for what to do with a FetchResponse object: <br>
https://pyodide.org/en/stable/usage/api/python-api.html#pyodide.http.FetchResponse
</p>
</div>
</body>
</html>
```
## Explanation
### `py-env` tag for importing our Python code
The very first thing to notice is the `py-env` tag. This tag is used to import Python files into the `PyScript`.
In this case, we are importing the `request.py` file, which contains the `request` function we wrote above.
### `py-script` tag for making async HTTP requests.
Next, the `py-script` tag contains the actual Python code where we import `asyncio` and `json`,
which are required or helpful for the `request` function.
The `# GET`, `# POST`, `# PUT`, `# DELETE` blocks show examples of how to use the `request` function to make basic
HTTP requests. The `await` keyword is required not only for the `request` function, but also for certain methods of the
`FetchResponse` object, such as `json()`, meaning that the code is asynchronous and slower requests will not block the
faster ones.
### HTTP Requests
HTTP requests are a very common way to communicate with a server. They are used for everything from getting data from
a database, to sending emails, to authorization, and more. Due to safety concerns, files loaded from the
local file system are not accessible by `PyScript`. Therefore, the proper way to load data into `PyScript` is also
through HTTP requests.
In our example, we show how to pass in a request `body`, `headers`, and specify the request `method`, in order to make
`GET`, `POST`, `PUT`, and `DELETE` requests, although methods such as `PATCH` are also available. Additional
parameters for the `fetch` API are also available, which can be specified as keyword arguments passed to our helper
function or to `pyfetch`. See the
[fetch documentation](https://developer.mozilla.org/en-US/docs/Web/API/fetch#parameters) for more information.
HTTP requests are defined by standards-setting bodies in [RFC 1945](https://www.rfc-editor.org/info/rfc1945) and
[RFC 9110](https://www.rfc-editor.org/info/rfc9110).
# Conclusion
This tutorial demonstrates how to make HTTP requests using `pyfetch` and the `FetchResponse` objects. Importing Python
code/files into the `PyScript` using the `py-env` tag is also covered.
Although a simple example, the principals here can be used to create complex web applications inside of `PyScript`,
or load data into `PyScript` for use by an application, all served as a static HTML page, which is pretty amazing!
# API Quick Reference
## pyodide.http.pyfetch
### Usage
```python
await pyodide.http.pyfetch(url: str, **kwargs: Any) -> FetchResponse
```
Use `pyfetch` to make HTTP requests in `PyScript`. This is a wrapper around the `fetch` API. Returns a `FetchResponse`.
### [`pyfetch` Docs.](https://pyodide.org/en/stable/usage/api/python-api/http.html#pyodide.http.pyfetch)
## pyodide.http.FetchResponse
### Usage
```python
response: pyodide.http.FetchResponse = await <pyfetch call>
status = response.status
json = await response.json()
```
Class for handling HTTP responses. This is a wrapper around the `JavaScript` fetch `Response`. Contains common (async)
methods and properties for handling HTTP responses, such as `json()`, `url`, `status`, `headers`, etc.
### [`FetchResponse` Docs.](https://pyodide.org/en/stable/usage/api/python-api/http.html#pyodide.http.FetchResponse)

View File

@@ -14,5 +14,6 @@ maxdepth: 2
glob: glob:
caption: 'Contents:' caption: 'Contents:'
--- ---
* passing-objects
http-requests
``` ```

View File

@@ -0,0 +1,140 @@
# How to Pass Objects from PyScript to Javascript (and Vice Versa)
[Pyodide](https://pyodide.org), the runtime that underlies PyScript, does a lot of work under the hood to translate objects between Python and JavaScript. This allows code in one language to access objects defined in the other.
This guide discusses how to pass objects between JavaScript and Python within PyScript. For more details on how Pyodide handles translating and proxying objects between the two languages, see the [Pyodide Type Translations Page](https://pyodide.org/en/stable/usage/type-conversions.html).
For our purposes, an 'object' is anything that can be bound to a variable (a number, string, object, [function](https://developer.mozilla.org/en-US/docs/Glossary/First-class_Function), etc).
## JavaScript to PyScript
We can use the syntax `from js import ...` to import JavaScript objects directly into PyScript. Simple JavaScript objects are converted to equivalent Python types; these are called [implicit conversions](https://pyodide.org/en/stable/usage/type-conversions.html#implicit-conversions). More complicated objects are wrapped in [JSProxy](https://pyodide.org/en/stable/usage/type-conversions.html) objects to make them behave like Python objects.
`import js` and `from js import ...` [in Pyodide](https://pyodide.org/en/stable/usage/type-conversions.html#type-translations-using-js-obj-from-py) get objects from the [JavaScript globalThis scope](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis), so keep the[ rules of JavaScript variable scoping](https://www.freecodecamp.org/news/var-let-and-const-whats-the-difference/) in mind.
```html
<script>
name = "Guido" //A JS variable
// Define a JS Function
function addTwoNumbers(x, y){
return x + y;
}
</script>
```
```python
<py-script>
# Import and use JS function and variable into Python
from js import name, addTwoNumbers
print(f"Hello {name}")
print("Adding 1 and 2 in Javascript: " + str(addTwoNumbers(1, 2)))
</py-script>
```
## PyScript to JavaScript
Since [PyScript doesn't export its instance of Pyodide](https://github.com/pyscript/pyscript/issues/494) and only one instance of Pyodide can be running in a browser window at a time, there isn't currently a way for Javascript to access Objects defined inside PyScript tags "directly".
We can work around this limitation using [JavaScript's eval() function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval), which executes a string as code much like [Python's eval()](https://docs.python.org/3/library/functions.html#eval). First, we create a JS function `createObject` which takes an object and a string, then uses `eval()` to create a variable named after the string and bind it to that object. By calling this function from PyScript (where we have access to the Pyodide global namespace), we can bind JavaScript variables to Python objects without having direct access to that global namespace.
Include the following script tag anywhere in your html document:
```html
<script>
function createObject(object, variableName){
//Bind a variable whose name is the string variableName
// to the object called 'object'
let execString = variableName + " = object"
console.log("Running '" + execString + "'");
eval(execString)
}
</script>
```
This function takes a Python Object and creates a variable pointing to it in the JavaScript global scope.
### Exporting all Global Python Objects
We can use our new `createObject` function to "export" the entire Python global object dictionary as a JavaScript object:
```python
<py-script>
from js import createObject
from pyodide.ffi import create_proxy
createObject(create_proxy(globals()), "pyodideGlobals")
</py-script>
```
This will make all Python global variables available in JavaScript with `pyodideGlobals.get('my_variable_name')`.
(Since PyScript tags evaluate _after_ all JavaScript on the page, we can't just dump a `console.log(...)` into a `<script>` tag, since that tag will evaluate before any PyScript has a chance to. We need to delay accessing the Python variable in JavaScript until after the Python code has a chance to run. The following example uses a button with `id="do-math"` to achieve this, but any method would be valid.)
```python
<py-script>
# create some Python objects:
symbols = {'pi': 3.1415926, 'e': 2.7182818}
def rough_exponential(x):
return symbols['e']**x
class Circle():
def __init__(self, radius):
self.radius = radius
@property
def area:
return symbols['pi'] * self.radius**2
</py-script>
```
```html
<input type="button" value="Log Python Variables" id="do-mmath">
<script>
document.getElementById("do-math").addEventListener("click", () => {
const exp = pyodideGlobals.get('rough_exponential');
console.log("e squared is about ${exp(2)}");
const c = pyodideGlobals.get('Circle')(4);
console.log("The area of c is ${c.area}");
});
</script>
```
### Exporting Individual Python Objects
We can also export individual Python objects to the JavaScript global scope if we wish.
(As above, the following example uses a button to delay the execution of the `<script>` until after the PyScript has run.)
```python
<py-script>
import js
from pyodide.ffi import create_proxy
# Create 3 python objects
language = "Python 3"
animals = ['dog', 'cat', 'bird']
multiply3 = lambda a, b, c: a * b * c
# js object can be named the same as Python objects...
js.createObject(language, "language")
# ...but don't have to be
js.createObject(create_proxy(animals), "animals_from_py")
# functions are objects too, in both Python and Javascript
js.createObject(create_proxy(multiply3), "multiply")
</py-script>
```
```html
<input type="button" value="Log Python Variables" id="log-python-variables">
<script>
document.getElementById("log-python-variables").addEventListener("click", () => {
console.log(`Nice job using ${language}`);
for (const animal of animals_from_py){
console.log(`Do you like ${animal}s? `);
}
console.log(`2 times 3 times 4 is ${multiply(2,3,4)}`);
});
</script>
```

BIN
docs/img/diataxis.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

View File

@@ -24,7 +24,10 @@ Check out our [getting started guide](tutorials/getting-started.md)!
::: :::
:::{grid-item-card} [How-to guides](howtos/index.md) :::{grid-item-card} [How-to guides](howtos/index.md)
**Coming soon!** You already know the basics and want to learn specifics!
[Passing Objects between JavaScript and Python](howtos/passing-objects.md)
[Making async HTTP requests in pure Python](howtos/http-requests.md)
::: :::
:::{grid-item-card} [Concepts](concepts/index.md) :::{grid-item-card} [Concepts](concepts/index.md)

View File

@@ -5,16 +5,50 @@ This page will guide you through getting started with PyScript.
## Development setup ## Development setup
PyScript does not require any development environment other PyScript does not require any development environment other
than a web browser. We recommend using [Chrome](https://www.google.com/chrome/). than a web browser (we recommend using [Chrome](https://www.google.com/chrome/)) and a text editor, even though using your [IDE](https://en.wikipedia.org/wiki/Integrated_development_environment) of choice might be convenient.
If you're using [VSCode](https://code.visualstudio.com/), the If you're using [VSCode](https://code.visualstudio.com/), the
[Live Server extension](https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer) [Live Server extension](https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer)
can be used to reload the page as you edit the HTML file. can be used to reload the page as you edit the HTML file.
## Trying before installing
If you're new to programming and know nothing about HTML or just want to try some of PyScript features, we recommend using the [REPL](https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop) element in the [PyScript REPL example](https://pyscript.net/examples/repl.html) instead so you can have a programming experience in a REPL that doesn't require any setup. This REPL can be used to have an interactive experience using Python directly.
Alternatively, you can also use an online editor like W3School's [TryIt Editor](https://www.w3schools.com/html/tryit.asp?filename=tryhtml_default_default) and just plug the code below into it, as shown in the [example](https://docs.pyscript.net/latest/concepts/what-is-pyscript.html#example) page and click the run button.
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>REPL</title>
<link rel="icon" type="image/png" href="favicon.png" />
<link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/latest/pyscript.js"></script>
</head>
<body>
Hello world! <br>
This is the current date and time, as computed by Python:
<py-script>
from datetime import datetime
now = datetime.now()
now.strftime("%m/%d/%Y, %H:%M:%S")
</py-script>
</body>
</html>
```
You could try changing the code above to explore and play with pyscript yourself.
## Installation ## Installation
There is no installation required. In this document, we'll use There is no installation required. In this document, we'll use
the PyScript assets served on https://pyscript.net. the PyScript assets served on [https://pyscript.net](https://pyscript.net).
If you want to download the source and build it yourself, follow If you want to download the source and build it yourself, follow
the instructions in the [README.md](https://github.com/pyscript/pyscript/blob/main/README.md) file. the instructions in the [README.md](https://github.com/pyscript/pyscript/blob/main/README.md) file.
@@ -31,8 +65,8 @@ open an HTML by double-clicking it in your file explorer.
```html ```html
<html> <html>
<head> <head>
<link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" /> <link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/alpha/pyscript.js"></script> <script defer src="https://pyscript.net/latest/pyscript.js"></script>
</head> </head>
<body> <py-script> print('Hello, World!') </py-script> </body> <body> <py-script> print('Hello, World!') </py-script> </body>
</html> </html>
@@ -50,8 +84,8 @@ print back onto the page. For example, we can compute π.
```html ```html
<html> <html>
<head> <head>
<link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" /> <link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/alpha/pyscript.js"></script> <script defer src="https://pyscript.net/latest/pyscript.js"></script>
</head> </head>
<body> <body>
<py-script> <py-script>
@@ -83,8 +117,8 @@ the `<py-script>` tag to write to.
```html ```html
<html> <html>
<head> <head>
<link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" /> <link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/alpha/pyscript.js"></script> <script defer src="https://pyscript.net/latest/pyscript.js"></script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
</head> </head>
@@ -109,7 +143,243 @@ the `<py-script>` tag to write to.
</html> </html>
``` ```
## The py-env tag ## The py-config tag
Use the `<py-config>` tag to set and configure general metadata along with declaring dependencies for your PyScript application. The configuration has to be set in either TOML or JSON format. If you are unfamiliar with JSON, consider reading [freecodecamp's JSON for beginners](https://www.freecodecamp.org/news/what-is-json-a-json-file-example/) guide for more information. And for TOML, consider reading about it [here](https://learnxinyminutes.com/docs/toml/).
The ideal place to use `<py-config>` in between the `<head>...</head>` tags.
The `<py-config>` tag can be used as follows:
```html
<py-config>
autoclose_loader = true
[[runtimes]]
src = "https://cdn.jsdelivr.net/pyodide/v0.21.2/full/pyodide.js"
name = "pyodide-0.21.2"
lang = "python"
</py-config>
```
Alternatively, a JSON config can be passed using the `type` attribute.
```html
<py-config type="json">
{
"autoclose_loader": true,
"runtimes": [{
"src": "https://cdn.jsdelivr.net/pyodide/v0.21.2/full/pyodide.js",
"name": "pyodide-0.21.2",
"lang": "python"
}]
}
</py-config>
```
Besides passing the config as inline (as shown above), one can also pass it with the `src` attribute. This is demonstrated below:
```
<py-config src="./custom.toml"></py-config>
```
where `custom.toml` contains
```
autoclose_loader = true
[[runtimes]]
src = "https://cdn.jsdelivr.net/pyodide/v0.21.2/full/pyodide.js"
name = "pyodide-0.21.2"
lang = "python"
```
This can also be done via JSON using the `type` attribute. By default, `type` is set to `"toml"` if not supplied.
```
<py-config type="json" src="./custom.json"></py-config>
```
where `custom.json` contains
```
{
"autoclose_loader": true,
"runtimes": [{
"src": "https://cdn.jsdelivr.net/pyodide/v0.21.2/full/pyodide.js",
"name": "pyodide-0.21.2",
"lang": "python"
}]
}
```
One can also use both i.e pass the config from `src` attribute as well as specify it as `inline`. So the following snippet is also valid:
```
<py-config src="./custom.toml">
paths = ["./utils.py"]
</py-config>
```
This can also be done via JSON using the `type` attribute.
```
<py-config type="json" src="./custom.json">
{
"paths": ["./utils.py"]
}
</py-config>
```
Note: While the `<py-config>` tag supports both TOML and JSON, one cannot mix the type of config passed from 2 different sources i.e. the case when inline config is in TOML format while config from src is in JSON format is NOT allowed. Similarly for the opposite case.
---
This is helpful in cases where a number of applications share a common configuration (which can be supplied via `src`), but their specific keys need to be customised and overridden.
The keys supplied through `inline` override the values present in config supplied via `src`.
One can also declare dependencies so as to get access to many 3rd party OSS packages that are supported by PyScript.
You can also link to `.whl` files directly on disk like in our [toga example](https://github.com/pyscript/pyscript/blob/main/examples/toga/freedom.html).
```
<py-config>
packages = ["./static/wheels/travertino-0.1.3-py3-none-any.whl"]
</py-config>
```
OR in JSON like
```
<py-config type="json">
{
"packages": ["./static/wheels/travertino-0.1.3-py3-none-any.whl"]
}
</py-config>
```
If your `.whl` is not a pure Python wheel, then open a PR or issue with [pyodide](https://github.com/pyodide/pyodide) to get it added [here](https://github.com/pyodide/pyodide/tree/main/packages).
If there's enough popular demand, the pyodide team will likely work on supporting your package. Regardless, things will likely move faster if you make the PR and consult with the team to get unblocked.
For example, NumPy and Matplotlib are available. Notice here we're using `<py-script output="plot">`
as a shortcut, which takes the expression on the last line of the script and runs `pyscript.write('plot', fig)`.
```html
<html>
<head>
<link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/latest/pyscript.js"></script>
<py-config type="json">
{
"packages": ["numpy", "matplotlib"]
}
</py-config>
</head>
<body>
<h1>Let's plot random numbers</h1>
<div id="plot"></div>
<py-script output="plot">
import matplotlib.pyplot as plt
import numpy as np
x = np.random.randn(1000)
y = np.random.randn(1000)
fig, ax = plt.subplots()
ax.scatter(x, y)
fig
</py-script>
</body>
</html>
```
### Local modules
In addition to packages, you can declare local Python modules that will
be imported in the `<py-script>` tag. For example, we can place the random
number generation steps in a function in the file `data.py`.
```python
# data.py
import numpy as np
def make_x_and_y(n):
x = np.random.randn(n)
y = np.random.randn(n)
return x, y
```
In the HTML tag `<py-config>`, paths to local modules are provided in the
`paths:` key.
```html
<html>
<head>
<link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/latest/pyscript.js"></script>
<py-config type="toml">
packages = ["numpy", "matplotlib"]
paths = ["./data.py"]
</py-config>
</head>
<body>
<h1>Let's plot random numbers</h1>
<div id="plot"></div>
<py-script output="plot">
import matplotlib.pyplot as plt
from data import make_x_and_y
x, y = make_x_and_y(n=1000)
fig, ax = plt.subplots()
ax.scatter(x, y)
fig
</py-script>
</body>
</html>
```
The following optional values are supported by `<py-config>`:
| Value | Type | Description |
| ------ | ---- | ----------- |
| `name` | string | Name of the user application. This field can be any string and is to be used by the application author for their own customization purposes. |
| `description` | string | Description of the user application. This field can be any string and is to be used by the application author for their own customization purposes. |
| `version` | string | Version of the user application. This field can be any string and is to be used by the application author for their own customization purposes. It is not related to the PyScript version. |
| `schema_version` | number | The version of the config schema which determines what all keys are supported. This can be supplied by the user so PyScript knows what to expect in the config. If not supplied, the latest version for the schema is automatically used. |
| `type` | string | Type of the project. The default is an "app" i.e. a user application |
| `author_name` | string | Name of the author. |
| `author_email` | string | Email of the author. |
| `license` | string | License to be used for the user application. |
| `autoclose_loader` | boolean | If false, PyScript will not close the loading splash screen when the startup operations finish. |
| `packages` | List of Packages | Dependencies on 3rd party OSS packages are specified here. The default value is an empty list. |
| `paths` | List of Paths | Local Python modules are to be specified here. The default value is an empty list. |
| `plugins` | List of Plugins | List of Plugins are to be specified here. The default value is an empty list. |
| `runtimes` | List of Runtimes | List of runtime configurations, described below. The default value contains a single Pyodide based runtime. |
A runtime configuration consists of the following:
| Value | Type | Description |
| ----- | ---- | ----------- |
| `src` | string (Required) | URL to the runtime source. |
| `name` | string | Name of the runtime. This field can be any string and is to be used by the application author for their own customization purposes |
| `lang` | string | Programming language supported by the runtime. This field can be used by the application author to provide clarification. It currently has no implications on how PyScript behaves. |
Besides the above format, a user can also supply any extra keys and values that are relevant as metadata information or perhaps are being used within the application.
For example, a valid config could also be with the snippet below:
```
<py-config type="toml">
magic = "unicorn"
</py-config>
```
OR in JSON like
```
<py-config type="json">
{
"magic": "unicorn"
}
</py-config>
```
If this `"magic"` key is present in config supplied via `src` and also present in config supplied via `inline`, then the value in the inline config is given priority i.e. the overriding process also works for custom keys.
## The py-env tag (Deprecated)
**The <py-env> tag is deprecated as of `2022.09.1` release but you can still use the functionality explained below. It will be removed in the next release. To specify packages in the recommended way, please see the <py-config> section.**
In addition to the [Python Standard Library](https://docs.python.org/3/library/) and In addition to the [Python Standard Library](https://docs.python.org/3/library/) and
the `pyscript` module, many 3rd-party OSS packages will work out-of-the-box with PyScript. the `pyscript` module, many 3rd-party OSS packages will work out-of-the-box with PyScript.
@@ -132,8 +402,8 @@ as a shortcut, which takes the expression on the last line of the script and run
```html ```html
<html> <html>
<head> <head>
<link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" /> <link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/alpha/pyscript.js"></script> <script defer src="https://pyscript.net/latest/pyscript.js"></script>
<py-env> <py-env>
- numpy - numpy
- matplotlib - matplotlib
@@ -158,17 +428,16 @@ as a shortcut, which takes the expression on the last line of the script and run
</html> </html>
``` ```
### Local modules ### Local modules with py-env (Deprecated)
**The <py-env> tag is deprecated as of `2022.09.1` release but you can still use the functionality explained below. It will be removed in the next release. To specify local modules in the recommended way, please see the <py-config> section.**
In addition to packages, you can declare local Python modules that will In addition to packages, you can declare local Python modules that will
be imported in the `<py-script>` tag. For example, we can place the random be imported in the `<py-script>` tag. For example, we can place the random
number generation steps in a function in the file `data.py`. number generation steps in a function in the file `data.py`.
```python ```python
# data.py # data.py
import numpy as np import numpy as np
def make_x_and_y(n): def make_x_and_y(n):
x = np.random.randn(n) x = np.random.randn(n)
y = np.random.randn(n) y = np.random.randn(n)
@@ -181,8 +450,8 @@ In the HTML tag `<py-env>`, paths to local modules are provided in the
```html ```html
<html> <html>
<head> <head>
<link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" /> <link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/alpha/pyscript.js"></script> <script defer src="https://pyscript.net/latest/pyscript.js"></script>
<py-env> <py-env>
- numpy - numpy
- matplotlib - matplotlib
@@ -197,9 +466,7 @@ In the HTML tag `<py-env>`, paths to local modules are provided in the
<py-script output="plot"> <py-script output="plot">
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
from data import make_x_and_y from data import make_x_and_y
x, y = make_x_and_y(n=1000) x, y = make_x_and_y(n=1000)
fig, ax = plt.subplots() fig, ax = plt.subplots()
ax.scatter(x, y) ax.scatter(x, y)
fig fig
@@ -207,50 +474,20 @@ In the HTML tag `<py-env>`, paths to local modules are provided in the
</body> </body>
</html> </html>
``` ```
## The py-repl tag ## The py-repl tag
The `<py-repl>` tag creates a REPL component that is rendered to the page as a code editor, allowing you to write executable code inline. The `<py-repl>` tag creates a REPL component that is rendered to the page as a code editor, allowing you to write executable code inline.
```html ```html
<html> <html>
<head> <head>
<link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" /> <link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/alpha/pyscript.js"></script> <script defer src="https://pyscript.net/latest/pyscript.js"></script>
</head> </head>
<py-repl></py-repl> <py-repl></py-repl>
</html> </html>
``` ```
## The py-config tag
Use the `<py-config>` tag to set and configure general metadata about your PyScript application in YAML format. If you are unfamiliar with YAML, consider reading [Red Hat's YAML for beginners](https://www.redhat.com/sysadmin/yaml-beginners) guide for more information.
The `<py-config>` tag can be used as follows:
```html
<py-config>
autoclose_loader: false
runtimes:
- src: "https://cdn.jsdelivr.net/pyodide/v0.20.0/full/pyodide.js"
name: pyodide-0.20
lang: python
</py-config>
```
The following optional values are supported by `<py-config>`:
| Value | Type | Description |
| ------ | ---- | ----------- |
| `autoclose_loader` | boolean | If false, PyScript will not close the loading splash screen when the startup operations finish. |
| `name` | string | Name of the user application. This field can be any string and is to be used by the application author for their own customization purposes. |
| `version` | string | Version of the user application. This field can be any string and is to be used by the application author for their own customization purposes. It is not related to the PyScript version. |
| `runtimes` | List of Runtimes | List of runtime configurations, described below.
A runtime configuration consists of the following:
| Value | Type | Description |
| ----- | ---- | ----------- |
| `src` | string (Required) | URL to the runtime source. |
| `name` | string | Name of the runtime. This field can be any string and is to be used by the application author for their own customization purposes |
| `lang` | string | Programming language supported by the runtime. This field can be used by the application author to provide clarification. It currently has no implications on how PyScript behaves. |
## Visual component tags ## Visual component tags
The following tags can be used to add visual attributes to your HTML page. The following tags can be used to add visual attributes to your HTML page.

View File

@@ -3,13 +3,15 @@
<title>Altair</title> <title>Altair</title>
<meta charset="utf-8"> <meta charset="utf-8">
<link rel="icon" type="image/x-icon" href="./favicon.png"> <link rel="icon" type="image/x-icon" href="./favicon.png">
<link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" /> <link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/alpha/pyscript.js"></script> <script defer src="https://pyscript.net/latest/pyscript.js"></script>
<py-env> <py-config>
- altair packages = [
- pandas "altair",
- vega_datasets "pandas",
</py-env> "vega_datasets"
]
</py-config>
</head> </head>
<body> <body>
<div id="altair" style="width: 100%; height: 100%"></div> <div id="altair" style="width: 100%; height: 100%"></div>

View File

@@ -3,13 +3,14 @@
<title>Antigravity</title> <title>Antigravity</title>
<meta charset="utf-8"> <meta charset="utf-8">
<link rel="icon" type="image/x-icon" href="./favicon.png"> <link rel="icon" type="image/x-icon" href="./favicon.png">
<link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" /> <link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/alpha/pyscript.js"></script> <script defer src="https://pyscript.net/latest/pyscript.js"></script>
</head> </head>
<py-env> <py-config>
- paths: paths = [
- ./antigravity.py "./antigravity.py"
</py-env> ]
</py-config>
<body> <body>
<b>Based on xkcd: antigravity https://xkcd.com/353/.</b> <b>Based on xkcd: antigravity https://xkcd.com/353/.</b>
<py-script> <py-script>

View File

@@ -2,7 +2,7 @@ import random
import sys import sys
from js import DOMParser, document, setInterval from js import DOMParser, document, setInterval
from pyodide import create_proxy from pyodide.ffi import create_proxy
from pyodide.http import open_url from pyodide.http import open_url

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" />
<title>Async Await BLOCKING LOOP Pyscript Twice</title>
<link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/latest/pyscript.js"></script>
</head>
<body>
<py-script>
import js
import asyncio
for i in range(3):
js.console.log('A', i)
await asyncio.sleep(0.1)
</py-script>
<py-script>
import js
import asyncio
for i in range(3):
js.console.log('B', i)
await asyncio.sleep(0.1)
</py-script>
</body>
</html>

View File

@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Async Await BLOCKING LOOP Pyscript Twice</title>
<link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/latest/pyscript.js"></script>
</head>
<body>
<div>
Pyscript - FIRST ASYNC WITH INVOKED LOOP BLOCKING AWAIT AT SAME LEVEL AS LOOP Pyscript writing to console.log:
<py-script>
import js
import asyncio
async def asyncCallLoop1():
for i in range(3):
js.console.log('A', i)
await asyncio.sleep(2)
asyncCallLoop1()
</py-script>
</div>
<div>
Pyscript - SECOND ASYNC WITH INVOKED LOOP BLOCKING AWAIT AT SAME LEVEL AS LOOP Pyscript writing to console.log:
<py-script>
import js
import asyncio
async def asyncCallLoop2():
for i in range(3):
js.console.log('B', i)
await asyncio.sleep(2)
asyncCallLoop2()
</py-script>
</div>
</body>
</html>

View File

@@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Async Await BLOCKING LOOP Pyscript Twice</title>
<link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/latest/pyscript.js"></script>
</head>
<body>
<div>
Pyscript - FIRST ASYNC WITH INVOKED LOOP BLOCKING AWAIT AT SAME LEVEL AS LOOP Pyscript writing to console.log:
<py-script>
import js
import asyncio
async def asyncCallLoop1():
for i in range(3):
js.console.log('A', i)
await asyncio.sleep(2)
asyncCallLoop1()
</py-script>
</div>
<div>
Pyscript - SECOND ASYNC WITH TOP-LEVEL LOOP BLOCKING AWAIT AT SAME LEVEL AS LOOP Pyscript writing to console.log:
<py-script>
import js
import asyncio
for i in range(3):
js.console.log('B', i)
await asyncio.sleep(2)
</py-script>
</div>
</body>
</html>

View File

@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Async Await NON-BLOCKING Pyscript Twice</title>
<link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/latest/pyscript.js"></script>
</head>
<body>
<div>
Pyscript - FIRST ASYNC WITH NON-BLOCKING AWAIT AT ONE LEVEL LOWER THAN LOOP Pyscript writing to console.log:
<py-script>
import js
import asyncio
async def asyncCall1():
await asyncio.sleep(2)
for i in range(3):
js.console.log('A', i)
asyncCall1()
</py-script>
</div>
<div>
Pyscript - SECOND ASYNC WITH NON-BLOCKING AWAIT AT ONE LEVEL LOWER THAN LOOP Pyscript writing to console.log:
<py-script>
import js
import asyncio
async def asyncCall2():
await asyncio.sleep(2)
for i in range(3):
js.console.log('B', i)
asyncCall2()
</py-script>
</div>
</body>
</html>

View File

@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Async Await BLOCKING LOOP Pyscript Twice</title>
<link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/latest/pyscript.js"></script>
</head>
<body>
<div>
Pyscript - FIRST ASYNC WITH TOP-LEVEL LOOP BLOCKING AWAIT AT SAME LEVEL AS LOOP Pyscript writing to console.log:
<py-script>
import js
import asyncio
for i in range(3):
js.console.log('A', i)
await asyncio.sleep(2)
</py-script>
</div>
<div>
Pyscript - SECOND ASYNC WITH TOP-LEVEL LOOP BLOCKING AWAIT AT SAME LEVEL AS LOOP Pyscript writing to console.log:
<py-script>
import js
import asyncio
for i in range(3):
js.console.log('B', i)
await asyncio.sleep(2)
</py-script>
</div>
</body>
</html>

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" />
<title>Async Await BLOCKING LOOP Pyscript Twice</title>
<link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/latest/pyscript.js"></script>
</head>
<body>
<py-script>
import asyncio
from itertools import count
for i in count():
print(f"Count: {i}")
await asyncio.sleep(1)
</py-script>
</body>
</html>

View File

@@ -11,16 +11,18 @@
<script type="text/javascript"> <script type="text/javascript">
Bokeh.set_log_level("info"); Bokeh.set_log_level("info");
</script> </script>
<link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" /> <link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/alpha/pyscript.js"></script> <script defer src="https://pyscript.net/latest/pyscript.js"></script>
</head> </head>
<body> <body>
<py-env> <py-config>
- bokeh packages = [
- numpy "bokeh",
</py-env> "numpy"
]
</py-config>
<h1>Bokeh Example</h1> <h1>Bokeh Example</h1>
<div id="myplot"></div> <div id="myplot"></div>

View File

@@ -11,16 +11,18 @@
<script type="text/javascript"> <script type="text/javascript">
Bokeh.set_log_level("info"); Bokeh.set_log_level("info");
</script> </script>
<link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" /> <link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/alpha/pyscript.js"></script> <script defer src="https://pyscript.net/latest/pyscript.js"></script>
</head> </head>
<body> <body>
<py-env> <py-config>
- bokeh packages = [
- numpy "bokeh",
</py-env> "numpy"
]
</py-config>
<h1>Bokeh Example</h1> <h1>Bokeh Example</h1>
<div id="myplot"></div> <div id="myplot"></div>
@@ -71,17 +73,17 @@ def _link_docs(pydoc, jsdoc):
if getattr(event, 'setter_id', None) is not None: if getattr(event, 'setter_id', None) is not None:
return return
events = [event] events = [event]
json_patch = jsdoc.create_json_patch_string(pyodide.to_js(events)) json_patch = jsdoc.create_json_patch_string(pyodide.ffi.to_js(events))
pydoc.apply_json_patch(json.loads(json_patch)) pydoc.apply_json_patch(json.loads(json_patch))
jsdoc.on_change(pyodide.create_proxy(jssync), pyodide.to_js(False)) jsdoc.on_change(pyodide.ffi.create_proxy(jssync), pyodide.ffi.to_js(False))
def pysync(event): def pysync(event):
json_patch, buffers = process_document_events([event], use_buffers=True) json_patch, buffers = process_document_events([event], use_buffers=True)
buffer_map = {} buffer_map = {}
for (ref, buffer) in buffers: for (ref, buffer) in buffers:
buffer_map[ref['id']] = buffer buffer_map[ref['id']] = buffer
jsdoc.apply_json_patch(JSON.parse(json_patch), pyodide.to_js(buffer_map), setter_id='js') jsdoc.apply_json_patch(JSON.parse(json_patch), pyodide.ffi.to_js(buffer_map), setter_id='js')
pydoc.on_change(pysync) pydoc.on_change(pysync)

View File

@@ -4,8 +4,8 @@
<meta charset="utf-8"> <meta charset="utf-8">
<link rel="icon" type="image/x-icon" href="./favicon.png"> <link rel="icon" type="image/x-icon" href="./favicon.png">
<link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" /> <link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/alpha/pyscript.js"></script> <script defer src="https://pyscript.net/latest/pyscript.js"></script>
<style> <style>
.loading { .loading {
@@ -48,13 +48,13 @@
<script type="importmap"> <script type="importmap">
{ {
"imports": { "imports": {
"d3": "https://cdn.skypack.dev/d3@7" "d3": "https://cdn.skypack.dev/pin/d3@v7.6.1-1Q0NZ0WZnbYeSjDusJT3/mode=imports,min/optimized/d3.js"
} }
} }
</script> </script>
<script type="module"> <script type="module">
import * as d3 from "https://cdn.skypack.dev/d3@7"; import * as d3 from "https://cdn.skypack.dev/pin/d3@v7.6.1-1Q0NZ0WZnbYeSjDusJT3/mode=imports,min/optimized/d3.js";
const fruits = [ const fruits = [
{name: "🍊", count: 21}, {name: "🍊", count: 21},
@@ -109,7 +109,7 @@ for (const d of data) {
</script> </script>
<py-script> <py-script>
from pyodide import create_proxy, to_js from pyodide.ffi import create_proxy, to_js
import d3 import d3
fruits = [ fruits = [

View File

@@ -3,12 +3,14 @@
<title>Folium</title> <title>Folium</title>
<meta charset="utf-8"> <meta charset="utf-8">
<link rel="icon" type="image/x-icon" href="./favicon.png"> <link rel="icon" type="image/x-icon" href="./favicon.png">
<link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" /> <link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/alpha/pyscript.js"></script> <script defer src="https://pyscript.net/latest/pyscript.js"></script>
<py-env> <py-config>
- folium packages = [
- pandas "folium",
</py-env> "pandas"
]
</py-config>
</head> </head>
<body> <body>
<div id="folium" style="width: 100%; height: 100%"></div> <div id="folium" style="width: 100%; height: 100%"></div>

View File

@@ -7,9 +7,9 @@
<title>Svelte app</title> <title>Svelte app</title>
<link rel="icon" type="image/png" href="../favicon.png" /> <link rel="icon" type="image/png" href="../favicon.png" />
<link rel="stylesheet" href="../https://pyscript.net/alpha/pyscript.css" /> <link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="../https://pyscript.net/alpha/pyscript.js"></script> <script defer src="https://pyscript.net/latest/pyscript.js"></script>
</head> </head>
<body> <body>
@@ -106,7 +106,7 @@ pyscript.run_until_complete(start())
</py-script> </py-script>
<div class="mb10"> <div class="mb10">
<button id="trackbutton" class="bx--btn bx--btn--secondary" type="button" pys-onClick="toggle_video"> <button id="trackbutton" class="bx--btn bx--btn--secondary" type="button" py-onClick="toggle_video()">
Toggle Video Toggle Video
</button> </button>
<button id="nextimagebutton" class="mt10 bx--btn bx--btn--secondary" type="button" disabled> <button id="nextimagebutton" class="mt10 bx--btn bx--btn--secondary" type="button" disabled>

View File

@@ -7,9 +7,9 @@
<title>PyScript Hello World</title> <title>PyScript Hello World</title>
<link rel="icon" type="image/png" href="favicon.png" /> <link rel="icon" type="image/png" href="favicon.png" />
<link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" /> <link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/alpha/pyscript.js"></script> <script defer src="https://pyscript.net/latest/pyscript.js"></script>
</head> </head>
<body> <body>

View File

@@ -42,7 +42,7 @@
<h2>REPL</h2> <h2>REPL</h2>
</a> </a>
<p> <p>
A Python REPL (Read Eval Print Loop). A Python REPL (Read Eval Print Loop)
</p> </p>
</div> </div>
</div> </div>
@@ -53,7 +53,7 @@
<h2>REPL2</h2> <h2>REPL2</h2>
</a> </a>
<p> <p>
A Python REPL (Read Eval Print Loop) with slightly better formatting.. A Python REPL (Read Eval Print Loop) with slightly better formatting
</p> </p>
</div> </div>
</div> </div>
@@ -64,7 +64,7 @@
<h2>TODO App</h2> <h2>TODO App</h2>
</a> </a>
<p> <p>
Demo showing how would a Simple TODO App would look like in PyScript</code> tag Simple TODO App
</p> </p>
</div> </div>
</div> </div>
@@ -75,7 +75,7 @@
<h2>PyScript Native TODO App</h2> <h2>PyScript Native TODO App</h2>
</a> </a>
<p> <p>
Demo showing how would a Simple TODO App would look like in PyScript</code> tag Simple TODO App using <code>&lt;py-list&gt;</code>
</p> </p>
</div> </div>
</div> </div>
@@ -91,7 +91,7 @@
<h2>Matplotlib</h2> <h2>Matplotlib</h2>
</a> </a>
<p> <p>
Demonstrates rendering <a href="https://matplotlib.org/" target="_blank">Matplotlib</a> figure as output of the py-script tag Demonstrates rendering a <a href="https://matplotlib.org/" target="_blank">Matplotlib</a> figure as output of the py-script tag
</p> </p>
</div> </div>
</div> </div>
@@ -104,7 +104,7 @@
</h2> </h2>
</a> </a>
<p> <p>
Demonstrates rendering <a href="https://altair-viz.github.io/" target="_blank">Altair</a> plot as output of the py-script tag Demonstrates rendering a <a href="https://altair-viz.github.io/" target="_blank">Altair</a> plot as output of the py-script tag
</p> </p>
</div> </div>
</div> </div>
@@ -117,7 +117,7 @@
</h2> </h2>
</a> </a>
<p> <p>
Demonstrates rendering Demonstrates rendering a
<a href="https://python-visualization.github.io/folium/" target="_blank">Folium</a> <a href="https://python-visualization.github.io/folium/" target="_blank">Folium</a>
map as output of the py-script tag map as output of the py-script tag
</p> </p>

View File

@@ -7,9 +7,9 @@
<title>Svelte app</title> <title>Svelte app</title>
<link rel="icon" type="image/png" href="../favicon.png" /> <link rel="icon" type="image/png" href="../favicon.png" />
<link rel="stylesheet" href="../https://pyscript.net/alpha/pyscript.css" /> <link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="../https://pyscript.net/alpha/pyscript.js"></script> <script defer src="https://pyscript.net/latest/pyscript.js"></script>
</head> </head>
<body> <body>
@@ -72,7 +72,7 @@ def toggle_video(evt):
async def start_video(): async def start_video():
global isVideo global isVideo
update_note.write("Inside sstart video") update_note.write("Inside start video")
status = await handTrack.startVideo(video.element) status = await handTrack.startVideo(video.element)
console.log("video started", status) console.log("video started", status)
if status: if status:
@@ -131,7 +131,7 @@ pyscript.run_until_complete(start())
<div class="mb10"> <div class="mb10">
<p>Use < > to move, ↓ to crouch and x to jump. If video is enabled, say hi to jump as well! </p> <p>Use < > to move, ↓ to crouch and x to jump. If video is enabled, say hi to jump as well! </p>
<button id="trackbutton" class="bx--btn bx--btn--secondary" type="button" pys-onClick="toggle_video"> <button id="trackbutton" class="bx--btn bx--btn--secondary" type="button" py-onClick="toggle_video()">
Start Video Start Video
</button> </button>
<div id="update-note" py-mount class="updatenote mt10">loading model ..</div> <div id="update-note" py-mount class="updatenote mt10">loading model ..</div>

View File

@@ -3,11 +3,13 @@
<title>Matplotlib</title> <title>Matplotlib</title>
<meta charset="utf-8"> <meta charset="utf-8">
<link rel="icon" type="image/x-icon" href="./favicon.png"> <link rel="icon" type="image/x-icon" href="./favicon.png">
<link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" /> <link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/alpha/pyscript.js"></script> <script defer src="https://pyscript.net/latest/pyscript.js"></script>
<py-env> <py-config>
- matplotlib packages = [
</py-env> "matplotlib"
]
</py-config>
</head> </head>
<body> <body>
<div id="mpl"></div> <div id="mpl"></div>

View File

@@ -2,16 +2,18 @@
<head> <head>
<link rel="icon" type="image/x-icon" href="./favicon.png"> <link rel="icon" type="image/x-icon" href="./favicon.png">
<link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" /> <link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/alpha/pyscript.js"></script> <script defer src="https://pyscript.net/latest/pyscript.js"></script>
</head> </head>
<body> <body>
<py-env> <py-config>
- numpy packages = [
- networkx "numpy",
- matplotlib "networkx",
</py-env> "matplotlib"
]
</py-config>
<py-script> <py-script>
import numpy as np import numpy as np
import networkx as nx import networkx as nx

View File

@@ -6,14 +6,16 @@
<title>micrograd</title> <title>micrograd</title>
<link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" /> <link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/alpha/pyscript.js"></script> <script defer src="https://pyscript.net/latest/pyscript.js"></script>
<py-env> <py-config>
- micrograd packages = [
- numpy "micrograd",
- matplotlib "numpy",
</py-env> "matplotlib"
]
</py-config>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
</head> </head>
@@ -49,7 +51,7 @@
<div id="python-status">Python is currently starting. Please wait...</div> <div id="python-status">Python is currently starting. Please wait...</div>
</p> </p>
<p> <p>
<button id="run-all-button" class="btn btn-primary" type="submit" pys-onClick="run_all_micrograd_demo">Run All</button><br> <button id="run-all-button" class="btn btn-primary" type="submit" py-onClick="run_all_micrograd_demo()">Run All</button><br>
<py-script src="/micrograd_ai.py"></py-script> <py-script src="/micrograd_ai.py"></py-script>
<div id="micrograd-run-all-print-div"></div><br> <div id="micrograd-run-all-print-div"></div><br>
<div id="micrograd-run-all-fig1-div"></div> <div id="micrograd-run-all-fig1-div"></div>

View File

@@ -3,8 +3,8 @@
<title>Visualization of Mandelbrot, Julia and Newton sets with NumPy and HTML5 canvas</title> <title>Visualization of Mandelbrot, Julia and Newton sets with NumPy and HTML5 canvas</title>
<meta charset="utf-8"> <meta charset="utf-8">
<link rel="icon" type="image/x-icon" href="./favicon.png"> <link rel="icon" type="image/x-icon" href="./favicon.png">
<link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" /> <link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/alpha/pyscript.js"></script> <script defer src="https://pyscript.net/latest/pyscript.js"></script>
<style> <style>
.loading { .loading {
@@ -78,16 +78,21 @@
</div> </div>
</div> </div>
<py-env> <py-config type="json">
- numpy {
- sympy "packages": [
- paths: "numpy",
- ./palettes.py "sympy"
- ./fractals.py ],
</py-env> "paths": [
"./palettes.py",
"./fractals.py"
]
}
</py-config>
<py-script> <py-script>
from pyodide import to_js, create_proxy from pyodide.ffi import to_js, create_proxy
import numpy as np import numpy as np
import sympy import sympy

View File

@@ -7,14 +7,16 @@
<script type="text/javascript" src="https://cdn.bokeh.org/bokeh/release/bokeh-widgets-2.4.2.min.js"></script> <script type="text/javascript" src="https://cdn.bokeh.org/bokeh/release/bokeh-widgets-2.4.2.min.js"></script>
<script type="text/javascript" src="https://cdn.bokeh.org/bokeh/release/bokeh-tables-2.4.2.min.js"></script> <script type="text/javascript" src="https://cdn.bokeh.org/bokeh/release/bokeh-tables-2.4.2.min.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/@holoviz/panel@0.13.1/dist/panel.min.js"></script> <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/@holoviz/panel@0.13.1/dist/panel.min.js"></script>
<link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" /> <link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/alpha/pyscript.js"></script> <script defer src="https://pyscript.net/latest/pyscript.js"></script>
</head> </head>
<py-env> <py-config>
- bokeh packages = [
- numpy "bokeh",
- panel==0.13.1 "numpy",
</py-env> "panel==0.13.1"
]
</py-config>
<body> <body>
<h1>Panel Example</h1> <h1>Panel Example</h1>
<div id="simple_app"></div> <div id="simple_app"></div>

View File

@@ -38,16 +38,18 @@
padding: 0; padding: 0;
} }
</style> </style>
<link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" /> <link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/alpha/pyscript.js"></script> <script defer src="https://pyscript.net/latest/pyscript.js"></script>
</head> </head>
<py-env> <py-config>
- bokeh packages = [
- numpy "bokeh",
- pandas "numpy",
- panel==0.13.1 "pandas",
</py-env> "panel==0.13.1"
]
</py-config>
<body> <body>
<div class="container-fluid d-flex flex-column vh-100 overflow-hidden" id="container"> <div class="container-fluid d-flex flex-column vh-100 overflow-hidden" id="container">

View File

@@ -39,17 +39,19 @@
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.slim.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/js/bootstrap.bundle.min.js"></script>
<link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" /> <link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/alpha/pyscript.js"></script> <script defer src="https://pyscript.net/latest/pyscript.js"></script>
</head> </head>
<py-env> <py-config>
- altair packages = [
- numpy "altair",
- pandas "numpy",
- scikit-learn "pandas",
- panel==0.13.1 "scikit-learn",
</py-env> "panel==0.13.1"
]
</py-config>
<body> <body>
<div class="container-fluid d-flex flex-column vh-100 overflow-hidden" id="container"> <div class="container-fluid d-flex flex-column vh-100 overflow-hidden" id="container">

View File

@@ -30,16 +30,18 @@
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.slim.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/js/bootstrap.bundle.min.js"></script>
<link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" /> <link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/alpha/pyscript.js"></script> <script defer src="https://pyscript.net/latest/pyscript.js"></script>
</head> </head>
<py-env> <py-config>
- bokeh packages = [
- numpy "bokeh",
- pandas "numpy",
- panel==0.13.1 "pandas",
</py-env> "panel==0.13.1"
]
</py-config>
<body> <body>
<div class="container-fluid d-flex flex-column vh-100 overflow-hidden" id="container"> <div class="container-fluid d-flex flex-column vh-100 overflow-hidden" id="container">

View File

@@ -7,14 +7,15 @@
<title>REPL</title> <title>REPL</title>
<link rel="icon" type="image/png" href="favicon.png" /> <link rel="icon" type="image/png" href="favicon.png" />
<link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" /> <link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/alpha/pyscript.js"></script> <script defer src="https://pyscript.net/latest/pyscript.js"></script>
</head> </head>
<py-env> <py-config>
- paths: paths = [
- ./antigravity.py "./antigravity.py"
</py-env> ]
</py-config>
<body> <body>
<h1><b>pyscript REPL</b></h1> <h1><b>pyscript REPL</b></h1>

View File

@@ -7,23 +7,26 @@
<title>Custom REPL Example</title> <title>Custom REPL Example</title>
<link rel="icon" type="image/png" href="favicon.png" /> <link rel="icon" type="image/png" href="favicon.png" />
<link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" /> <link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<link rel="stylesheet" href="repl.css" /> <link rel="stylesheet" href="repl.css" />
<script defer src="https://pyscript.net/alpha/pyscript.js"></script> <script defer src="https://pyscript.net/latest/pyscript.js"></script>
</head> </head>
<py-env> <py-config>
- bokeh packages = [
- numpy "bokeh",
- paths: "numpy"
- ./utils.py ]
- ./antigravity.py paths = [
</py-env> "./utils.py",
"./antigravity.py"
]
</py-config>
<body> <body>
<h1 class="font-semibold text-2xl ml-5">Custom REPL</h1> <h1 class="font-semibold text-2xl ml-5">Custom REPL</h1>
<py-box widths="1/2;1/2"> <py-box widths="2/3;1/3">
<py-repl id="my-repl" auto-generate="true" std-out="output" std-err="err-div"> </py-repl> <py-repl id="my-repl" auto-generate="true" std-out="output" std-err="err-div"> </py-repl>
<div id="output" class="p-4"></div> <div id="output" class="p-4"></div>
</py-box> </py-box>

View File

@@ -8,7 +8,7 @@
<meta name='viewport' content='width=device-width, initial-scale=1'> <meta name='viewport' content='width=device-width, initial-scale=1'>
<link rel='stylesheet' type='text/css' media='screen' href='https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css'> <link rel='stylesheet' type='text/css' media='screen' href='https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css'>
<link rel="icon" type="image/png" href="https://user-images.githubusercontent.com/49681382/166738771-d0c26557-426c-4688-9641-8db5e6b08348.png" /> <link rel="icon" type="image/png" href="https://user-images.githubusercontent.com/49681382/166738771-d0c26557-426c-4688-9641-8db5e6b08348.png" />
<script defer src="https://pyscript.net/alpha/pyscript.js"></script> <script defer src="https://pyscript.net/latest/pyscript.js"></script>
</head> </head>
<body> <body>
@@ -49,8 +49,8 @@
</div> </div>
</div> </div>
<div class="control"> <div class="control">
<button id="run" type="button" class="button is-primary" pys-onClick="run">Run!</button> <button id="run" type="button" class="button is-primary" py-onClick="run()">Run!</button>
<button id="clear" type="button" class="button is-danger" pys-onClick="clear">Clear</button> <button id="clear" type="button" class="button is-danger" py-onClick="clear()">Clear</button>
</div> </div>
</div> </div>

View File

@@ -7,13 +7,14 @@
<title>Simple Clock Demo</title> <title>Simple Clock Demo</title>
<link rel="icon" type="image/png" href="favicon.png" /> <link rel="icon" type="image/png" href="favicon.png" />
<link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" /> <link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/alpha/pyscript.js"></script> <script defer src="https://pyscript.net/latest/pyscript.js"></script>
<py-env> <py-config>
- paths: paths = [
- ./utils.py "./utils.py"
</py-env> ]
</py-config>
</head> </head>
<body> <body>

View File

@@ -6,13 +6,14 @@
<title>Todo App</title> <title>Todo App</title>
<link rel="icon" type="image/png" href="favicon.png" /> <link rel="icon" type="image/png" href="favicon.png" />
<link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" /> <link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/alpha/pyscript.js"></script> <script defer src="https://pyscript.net/latest/pyscript.js"></script>
<py-env> <py-config>
- paths: paths = [
- ./utils.py "./utils.py"
</py-env> ]
</py-config>
<py-register-widget src="./pylist.py" name="py-list" klass="PyList"></py-register-widget> <py-register-widget src="./pylist.py" name="py-list" klass="PyList"></py-register-widget>
<py-script> <py-script>

View File

@@ -7,28 +7,29 @@
<title>Todo App</title> <title>Todo App</title>
<link rel="icon" type="image/png" href="favicon.png" /> <link rel="icon" type="image/png" href="favicon.png" />
<link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" /> <link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/alpha/pyscript.js"></script> <script defer src="https://pyscript.net/latest/pyscript.js"></script>
<py-env> <py-config>
- paths: paths = [
- ./utils.py "./utils.py"
</py-env> ]
</py-config>
</head> </head>
<body class="container"> <body class="container">
<!-- <py-repl id="my-repl" auto-generate="true"> </py-repl> --> <!-- <py-repl id="my-repl" auto-generate="true"> </py-repl> -->
<py-script src="./todo.py"> </py-script> <py-script src="./todo.py"> </py-script>
<main class="max-w-xs mx-auto mt-4"> <main>
<section> <section>
<div class="text-center w-full mb-8"> <div class="text-center w-full mb-8">
<h1 class="text-3xl font-bold text-gray-800 uppercase tracking-tight">To Do List</h1> <h1 class="text-3xl font-bold text-gray-800 uppercase tracking-tight">To Do List</h1>
</div> </div>
<div> <div>
<input id="new-task-content" class="border flex-1 mr-3 border-gray-300 p-2 rounded" type="text"> <input id="new-task-content" class="py-input" type="text">
<button id="new-task-btn" class="p-2 text-white bg-blue-600 border border-blue-600 rounded" type="submit" pys-onClick="add_task"> <button id="new-task-btn" class="py-button" type="submit" py-click="add_task()">
Add task Add task
</button> </button>
</div> </div>
@@ -38,9 +39,9 @@
</div> </div>
<template id="task-template"> <template id="task-template">
<section class="task bg-white my-1"> <section class="task py-li-element">
<label for="flex items-center p-2 "> <label for="flex items-center p-2 ">
<input class="mr-2" type="checkbox" class="task-check"> <input class="mr-2" type="checkbox">
<p class="m-0 inline"></p> <p class="m-0 inline"></p>
</label> </label>
</section> </section>

View File

@@ -28,7 +28,7 @@ def add_task(*ags, **kws):
# add the task element to the page as new node in the list by cloning from a # add the task element to the page as new node in the list by cloning from a
# template # template
task_html = task_template.clone(task_id, to=task_list) task_html = task_template.clone(task_id)
task_html_content = task_html.select("p") task_html_content = task_html.select("p")
task_html_content.element.innerText = task["content"] task_html_content.element.innerText = task["content"]
task_html_check = task_html.select("input") task_html_check = task_html.select("input")

View File

@@ -12,7 +12,7 @@
crossorigin="anonymous"> crossorigin="anonymous">
<link rel="stylesheet" href="./static/toga.css"> <link rel="stylesheet" href="./static/toga.css">
<script defer src="https://pyscript.net/alpha/pyscript.js"></script> <script defer src="https://pyscript.net/latest/pyscript.js"></script>
<title>Loading...</title> <title>Loading...</title>
<link rel="icon" type="image/png" href="../favicon.png" /> <link rel="icon" type="image/png" href="../favicon.png" />
@@ -34,12 +34,14 @@
crossorigin="anonymous"> crossorigin="anonymous">
</script> </script>
</body> </body>
<py-env> <py-config>
- './static/wheels/travertino-0.1.3-py3-none-any.whl' packages = [
- './static/wheels/toga_core-0.3.0.dev33-py3-none-any.whl' "./static/wheels/travertino-0.1.3-py3-none-any.whl",
- './static/wheels/toga_web-0.3.0.dev33-py3-none-any.whl' "./static/wheels/toga_core-0.3.0.dev33-py3-none-any.whl",
- './static/wheels/freedom-0.0.1-py3-none-any.whl' "./static/wheels/toga_web-0.3.0.dev33-py3-none-any.whl",
</py-env> "./static/wheels/freedom-0.0.1-py3-none-any.whl"
]
</py-config>
<py-script> <py-script>
from toga_web.dom import handle as dom_handle from toga_web.dom import handle as dom_handle

View File

@@ -2,7 +2,7 @@ from datetime import datetime as dt
def format_date(dt_, fmt="%m/%d/%Y, %H:%M:%S"): def format_date(dt_, fmt="%m/%d/%Y, %H:%M:%S"):
return dt_.strftime(fmt) return f"{dt_:{fmt}}"
def now(fmt="%m/%d/%Y, %H:%M:%S"): def now(fmt="%m/%d/%Y, %H:%M:%S"):

View File

@@ -22,19 +22,16 @@
</div> </div>
<script src='https://cdnjs.cloudflare.com/ajax/libs/three.js/89/three.min.js'></script> <script src='https://cdnjs.cloudflare.com/ajax/libs/three.js/89/three.min.js'></script>
<script defer src="https://pyscript.net/alpha/pyscript.js"></script> <script defer src="https://pyscript.net/latest/pyscript.js"></script>
<link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" /> <link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script>
</script>
<py-script> <py-script>
from pyodide import create_proxy, to_js from pyodide.ffi import create_proxy, to_js
from js import window from js import window
from js import Math from js import Math
from js import THREE from js import THREE
from js import performance from js import performance
from pyodide import to_js
from js import Object from js import Object
import asyncio
@@ -53,7 +50,7 @@ def onMouseMove(event):
event.preventDefault(); event.preventDefault();
mouse.x = (event.clientX / window.innerWidth) * 2 - 1; mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
js.document.addEventListener('mousemove', pyodide.create_proxy(onMouseMove)) js.document.addEventListener('mousemove', pyodide.ffi.create_proxy(onMouseMove))
camera = THREE.PerspectiveCamera.new( 35, window.innerWidth / window.innerHeight, 1, 500 ) camera = THREE.PerspectiveCamera.new( 35, window.innerWidth / window.innerHeight, 1, 500 )
scene = THREE.Scene.new() scene = THREE.Scene.new()

View File

@@ -10,25 +10,12 @@ module.exports = {
sourceType: 'module', sourceType: 'module',
tsconfigRootDir: __dirname, tsconfigRootDir: __dirname,
project: ['./tsconfig.json'], project: ['./tsconfig.json'],
extraFileExtensions: ['.svelte'],
}, },
env: { env: {
es6: true, es6: true,
browser: true, browser: true,
}, },
overrides: [ plugins: ['@typescript-eslint'],
{
files: ['*.svelte'],
processor: 'svelte3/svelte3',
},
],
settings: {
'svelte3/typescript': require('typescript'),
// ignore style tags in Svelte because of Tailwind CSS
// See https://github.com/sveltejs/eslint-plugin-svelte3/issues/70
'svelte3/ignore-styles': () => true,
},
plugins: ['svelte3', '@typescript-eslint'],
ignorePatterns: ['node_modules'], ignorePatterns: ['node_modules'],
rules: { rules: {
'no-prototype-builtins': 'warn', 'no-prototype-builtins': 'warn',

View File

@@ -3,11 +3,7 @@ module.exports = {
bracketSameLine: true, bracketSameLine: true,
singleQuote: true, singleQuote: true,
printWidth: 120, printWidth: 120,
plugins: ['prettier-plugin-svelte'],
semi: true, semi: true,
svelteSortOrder: 'options-styles-scripts-markup',
svelteStrictMode: false,
svelteIndentScriptAndStyle: true,
tabWidth: 4, tabWidth: 4,
trailingComma: 'all', trailingComma: 'all',
} };

View File

@@ -2,20 +2,44 @@ tag := latest
git_hash ?= $(shell git log -1 --pretty=format:%h) git_hash ?= $(shell git log -1 --pretty=format:%h)
base_dir ?= $(shell git rev-parse --show-toplevel) base_dir ?= $(shell git rev-parse --show-toplevel)
src_dir ?= $(base_dir)/src src_dir ?= $(base_dir)/pyscriptjs/src
examples ?= ../$(base_dir)/examples examples ?= ../$(base_dir)/examples
app_dir ?= $(shell git rev-parse --show-prefix) app_dir ?= $(shell git rev-parse --show-prefix)
CONDA_EXE := conda CONDA_EXE := conda
CONDA_ENV ?= ./env CONDA_ENV ?= $(base_dir)/pyscriptjs/env
env := $(CONDA_ENV) env := $(CONDA_ENV)
conda_run := $(CONDA_EXE) run -p $(env) conda_run := $(CONDA_EXE) run -p $(env)
PYTEST_EXE := $(CONDA_ENV)/bin/pytest
GOOD_NODE_VER := 14
GOOD_NPM_VER := 6
NODE_VER := $(shell node -v | cut -d. -f1 | sed 's/^v\(.*\)/\1/')
NPM_VER := $(shell npm -v | cut -d. -f1)
ifeq ($(shell uname -s), Darwin)
SED_I_ARG := -i ''
else
SED_I_ARG := -i
endif
GOOD_NODE := $(shell if [ $(NODE_VER) -ge $(GOOD_NODE_VER) ]; then echo true; else echo false; fi)
GOOD_NPM := $(shell if [ $(NPM_VER) -ge $(GOOD_NPM_VER) ]; then echo true; else echo false; fi)
.PHONY: check-node
check-node:
@echo Build requires Node $(GOOD_NODE_VER).x or higher: $(NODE_VER) detected && $(GOOD_NODE)
.PHONY: check-npm
check-npm:
@echo Build requires npm $(GOOD_NPM_VER).x or higher: $(NPM_VER) detected && $(GOOD_NPM)
setup: setup:
make check-node
make check-npm
npm install npm install
$(CONDA_EXE) env $(shell [ -d $(env) ] && echo update || echo create) -p $(env) --file environment.yml $(CONDA_EXE) env $(shell [ -d $(env) ] && echo update || echo create) -p $(env) --file environment.yml
$(conda_run) playwright install $(conda_run) playwright install
$(CONDA_EXE) install -c anaconda pytest $(CONDA_EXE) install -c anaconda pytest -y
clean: clean:
find . -name \*.py[cod] -delete find . -name \*.py[cod] -delete
@@ -34,26 +58,41 @@ dev:
build: build:
npm run build npm run build
example: examples:
mkdir -p ./examples mkdir -p ./examples
cp -r ../examples/* ./examples cp -r ../examples/* ./examples
chmod -R 755 examples chmod -R 755 examples
find ./examples/toga -type f -name '*.html' -exec sed -i '' s+https://pyscript.net/alpha/+../build/+g {} \; find ./examples/toga -type f -name '*.html' -exec sed $(SED_I_ARG) s+https://pyscript.net/latest/+../../build/+g {} \;
find ./examples/webgl -type f -name '*.html' -exec sed -i '' s+https://pyscript.net/alpha/+../../build/+g {} \; find ./examples/webgl -type f -name '*.html' -exec sed $(SED_I_ARG) s+https://pyscript.net/latest/+../../../build/+g {} \;
find ./examples -type f -name '*.html' -exec sed -i '' s+https://pyscript.net/alpha/+./build/+g {} \; find ./examples -type f -name '*.html' -exec sed $(SED_I_ARG) s+https://pyscript.net/latest/+../build/+g {} \;
npm run build
rm -rf ./examples/build
mkdir -p ./examples/build
cp -R ./build/* ./examples/build
@echo "To serve examples run: $(conda_run) python -m http.server 8080 --directory examples"
# run prerequisites and serve pyscript examples at http://localhost:8000/examples/
run-examples: setup build examples
make examples
npm install
make dev
test: test:
make example make examples
npm run build make test-ts
$(conda_run) pytest -vv $(ARGS) tests/ --log-cli-level=warning make test-py
test-integration:
make examples
$(PYTEST_EXE) -vv $(ARGS) tests/integration/ --log-cli-level=warning
test-py: test-py:
@echo "Tests are coming :( this is a placeholder and it's meant to fail!" @echo "Tests from $(src_dir)"
$(conda_run) pytest -vv $(ARGS) tests/ --log-cli-level=warning $(PYTEST_EXE) -vv $(ARGS) tests/py-unit/ --log-cli-level=warning
test-ts: test-ts:
@echo "Tests are coming :( this is a placeholder and it's meant to fail!" @echo "Tests are coming :( this is a placeholder and it's meant to fail!"
npm run tests npm run test
fmt: fmt-py fmt-ts fmt: fmt-py fmt-ts
@echo "Format completed" @echo "Format completed"
@@ -74,10 +113,4 @@ fmt-py:
fmt-py-check: fmt-py-check:
$(conda_run) black -l 88 --check . $(conda_run) black -l 88 --check .
lint: lint-ts
@echo "Format check completed"
lint-ts:
$(conda_run) npm run lint
.PHONY: $(MAKECMDGOALS) .PHONY: $(MAKECMDGOALS)

View File

@@ -0,0 +1,12 @@
/**
* this file mocks the `src/python/pyscript.py` file
* since importing of `.py` files isn't usually supported
* inside JS/TS files.
*
* It sets the value of whatever is imported from
* `src/python/pyscript.py` to be an empty string i.e. ""
*
* This is needed since the imported object is further
* passed to a function which only accepts a string.
*/
module.exports = "";

View File

@@ -4,14 +4,16 @@ channels:
- microsoft - microsoft
dependencies: dependencies:
- python=3.9 - python=3.9
- pip=20.2.2 - pip
- pytest=7 - pytest=7
- nodejs=16 - nodejs=16
- black - black
- isort - isort
- codespell - codespell
- pre-commit - pre-commit
- playwright - pillow
- numpy
- pip: - pip:
- playwright
- pytest-playwright - pytest-playwright

19
pyscriptjs/jest.config.js Normal file
View File

@@ -0,0 +1,19 @@
//jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jest-environment-jsdom',
extensionsToTreatAsEsm: ['.ts'],
globals: {
'ts-jest': {
tsconfig: 'tsconfig.json',
useESM: true
}
},
verbose: true,
testEnvironmentOptions: {
url: "http://localhost"
},
moduleNameMapper: {
"^[./a-zA-Z0-9$_-]+\\.py$": "<rootDir>/__mocks__/fileMock.js",
}
};

File diff suppressed because it is too large Load Diff

View File

@@ -7,39 +7,43 @@
"dev": "rollup -c -w", "dev": "rollup -c -w",
"start": "sirv public --no-clear --port 8080", "start": "sirv public --no-clear --port 8080",
"validate": "svelte-check", "validate": "svelte-check",
"format:check": "prettier --check './src/**/*.{js,svelte,html,ts}'", "format:check": "prettier --check './src/**/*.{js,html,ts}'",
"format": "prettier --write './src/**/*.{js,svelte,html,ts}'", "format": "prettier --write './src/**/*.{js,html,ts}'",
"lint": "eslint './src/**/*.{js,svelte,html,ts}'", "lint": "eslint './src/**/*.{js,html,ts}'",
"lint:fix": "eslint --fix './src/**/*.{js,svelte,html,ts}'", "lint:fix": "eslint --fix './src/**/*.{js,html,ts}'",
"xprelint": "npm run format" "xprelint": "npm run format",
"test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --coverage",
"test:watch": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --watch"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-commonjs": "^17.0.0", "@rollup/plugin-commonjs": "^17.0.0",
"@rollup/plugin-node-resolve": "^11.0.0", "@rollup/plugin-node-resolve": "^11.0.0",
"@rollup/plugin-typescript": "^8.3.2", "@rollup/plugin-typescript": "^8.4.0",
"@tsconfig/svelte": "^1.0.0", "@tsconfig/svelte": "^1.0.0",
"@types/jest": "^28.1.6",
"@types/js-yaml": "^4.0.5", "@types/js-yaml": "^4.0.5",
"@typescript-eslint/eslint-plugin": "^5.20.0", "@types/node": "^18.7.11",
"@typescript-eslint/parser": "^5.20.0", "@typescript-eslint/eslint-plugin": "^5.36.0",
"@typescript-eslint/parser": "^5.36.0",
"autoprefixer": "^10.4.7", "autoprefixer": "^10.4.7",
"cross-env": "^7.0.3",
"eslint": "^8.14.0", "eslint": "^8.14.0",
"eslint-plugin-svelte3": "^3.4.1", "jest": "^28.1.3",
"postcss": "^8.4.13", "jest-environment-jsdom": "^28.1.3",
"prettier": "^2.6.2", "prettier": "^2.6.2",
"prettier-plugin-svelte": "^2.7.0", "pyodide": "0.21.2",
"rollup": "^2.71.1", "rollup": "^2.71.1",
"rollup-plugin-copy": "^3.4.0",
"rollup-plugin-css-only": "^3.1.0", "rollup-plugin-css-only": "^3.1.0",
"rollup-plugin-livereload": "^2.0.0", "rollup-plugin-livereload": "^2.0.0",
"rollup-plugin-serve": "^1.1.0", "rollup-plugin-serve": "^1.1.0",
"rollup-plugin-string": "^3.0.0", "rollup-plugin-string": "^3.0.0",
"rollup-plugin-svelte": "^7.0.0",
"rollup-plugin-terser": "^7.0.0", "rollup-plugin-terser": "^7.0.0",
"svelte": "^3.48.0", "svelte": "^3.48.0",
"svelte-check": "^1.0.0", "svelte-check": "^1.0.0",
"svelte-preprocess": "^4.10.6", "ts-jest": "^28.0.7",
"tailwindcss": "^2.0.2",
"tslib": "^2.4.0", "tslib": "^2.4.0",
"typescript": "^4.6.4" "typescript": "^4.8.2"
}, },
"dependencies": { "dependencies": {
"@codemirror/basic-setup": "^0.19.1", "@codemirror/basic-setup": "^0.19.1",

View File

@@ -1,13 +1,12 @@
import svelte from "rollup-plugin-svelte";
import commonjs from "@rollup/plugin-commonjs"; import commonjs from "@rollup/plugin-commonjs";
import resolve from "@rollup/plugin-node-resolve"; import resolve from "@rollup/plugin-node-resolve";
import livereload from "rollup-plugin-livereload"; import livereload from "rollup-plugin-livereload";
import { terser } from "rollup-plugin-terser"; import { terser } from "rollup-plugin-terser";
import sveltePreprocess from "svelte-preprocess";
import typescript from "@rollup/plugin-typescript"; import typescript from "@rollup/plugin-typescript";
import css from "rollup-plugin-css-only"; import css from "rollup-plugin-css-only";
import serve from "rollup-plugin-serve"; import serve from "rollup-plugin-serve";
import { string } from "rollup-plugin-string"; import { string } from "rollup-plugin-string";
import copy from 'rollup-plugin-copy'
const production = !process.env.ROLLUP_WATCH || (process.env.NODE_ENV === "production"); const production = !process.env.ROLLUP_WATCH || (process.env.NODE_ENV === "production");
@@ -17,28 +16,19 @@ export default {
{ {
sourcemap: true, sourcemap: true,
format: "iife", format: "iife",
inlineDynamicImports: true,
name: "app", name: "app",
file: "examples/build/pyscript.js", file: "build/pyscript.js",
}, },
{ {
file: "examples/build/pyscript.min.js", file: "build/pyscript.min.js",
format: "iife", format: "iife",
sourcemap: true, sourcemap: true,
inlineDynamicImports: true,
plugins: [terser()], plugins: [terser()],
}, },
], ],
plugins: [ plugins: [
svelte({
// add postcss config with tailwind
preprocess: sveltePreprocess({
postcss: {
plugins: [require("tailwindcss"), require("autoprefixer")],
},
}),
compilerOptions: {
dev: !production,
},
}),
css({ output: "pyscript.css" }), css({ output: "pyscript.css" }),
// Bundle all the Python files into the output file // Bundle all the Python files into the output file
string({ string({
@@ -53,6 +43,12 @@ export default {
sourceMap: !production, sourceMap: !production,
inlineSources: !production, inlineSources: !production,
}), }),
// This will make sure that examples will always get the latest build folder
!production && copy({
targets: [
{ src: 'build/*', dest: 'examples/build' }
]
}),
!production && serve(), !production && serve(),
!production && livereload("public"), !production && livereload("public"),
// production && terser(), // production && terser(),

View File

@@ -1,39 +0,0 @@
<style global>
.spinner::after {
content: '';
box-sizing: border-box;
width: 40px;
height: 40px;
position: absolute;
top: calc(40% - 20px);
left: calc(50% - 20px);
border-radius: 50%;
}
.spinner.smooth::after {
border-top: 4px solid rgba(255, 255, 255, 1);
border-left: 4px solid rgba(255, 255, 255, 1);
border-right: 4px solid rgba(255, 255, 255, 0);
animation: spinner 0.6s linear infinite;
}
@keyframes spinner {
to {
transform: rotate(360deg);
}
}
.label {
text-align: center;
width: 100%;
display: block;
color: rgba(255, 255, 255, 0.8);
font-size: 0.8rem;
margin-top: 6rem;
}
</style>
<script lang="ts">
import Tailwind from './Tailwind.svelte';
</script>
<Tailwind />

View File

@@ -1,5 +0,0 @@
<style global lang="postcss">
@tailwind base;
@tailwind components;
@tailwind utilities;
</style>

View File

@@ -1,26 +1,18 @@
import { loadedEnvironments, mode, pyodideLoaded } from '../stores'; import { runtimeLoaded } from '../stores';
import { guidGenerator, addClasses, removeClasses } from '../utils'; import { guidGenerator, addClasses, removeClasses } from '../utils';
// Premise used to connect to the first available pyodide interpreter
let runtime; import type { Runtime } from '../runtime';
let environments; import { getLogger } from '../logger';
let currentMode;
const logger = getLogger('pyscript/base');
// Global `Runtime` that implements the generic runtimes API
let runtime: Runtime;
let Element; let Element;
pyodideLoaded.subscribe(value => { runtimeLoaded.subscribe(value => {
runtime = value; runtime = value;
}); });
loadedEnvironments.subscribe(value => {
environments = value;
});
mode.subscribe(value => {
currentMode = value;
});
// TODO: use type declaractions
type PyodideInterface = {
registerJsModule(name: string, module: object): void;
};
export class BaseEvalElement extends HTMLElement { export class BaseEvalElement extends HTMLElement {
shadow: ShadowRoot; shadow: ShadowRoot;
@@ -60,7 +52,7 @@ export class BaseEvalElement extends HTMLElement {
this.appendOutput = false; this.appendOutput = false;
break; break;
default: default:
console.log(`${this.id}: custom output-modes are currently not implemented`); logger.warn(`${this.id}: custom output-modes are currently not implemented`);
} }
} }
@@ -90,20 +82,23 @@ export class BaseEvalElement extends HTMLElement {
return this.code; return this.code;
} }
protected async _register_esm(pyodide: PyodideInterface): Promise<void> { protected async _register_esm(runtime: Runtime): Promise<void> {
const imports: { [key: string]: unknown } = {}; const imports: { [key: string]: unknown } = {};
const nodes = document.querySelectorAll("script[type='importmap']");
for (const node of document.querySelectorAll("script[type='importmap']")) { const importmaps: Array<any> = [];
const importmap = (() => { nodes.forEach( node =>
{
let importmap;
try { try {
return JSON.parse(node.textContent); importmap = JSON.parse(node.textContent);
if (importmap?.imports == null) return;
importmaps.push(importmap);
} catch { } catch {
return null; return;
} }
})(); }
)
if (importmap?.imports == null) continue; for (const importmap of importmaps){
for (const [name, url] of Object.entries(importmap.imports)) { for (const [name, url] of Object.entries(importmap.imports)) {
if (typeof name != 'string' || typeof url != 'string') continue; if (typeof name != 'string' || typeof url != 'string') continue;
@@ -112,42 +107,32 @@ export class BaseEvalElement extends HTMLElement {
// "can't read 'name' of undefined" at import time // "can't read 'name' of undefined" at import time
imports[name] = { ...(await import(url)) }; imports[name] = { ...(await import(url)) };
} catch { } catch {
console.error(`failed to fetch '${url}' for '${name}'`); logger.error(`failed to fetch '${url}' for '${name}'`);
} }
} }
} }
pyodide.registerJsModule('esm', imports); runtime.registerJsModule('esm', imports);
} }
async evaluate(): Promise<void> { async evaluate(): Promise<void> {
console.log('evaluate');
this.preEvaluate(); this.preEvaluate();
const pyodide = runtime;
let source: string; let source: string;
let output; let output: string;
try { try {
source = this.source ? await this.getSourceFromFile(this.source) source = this.source ? await this.getSourceFromFile(this.source)
: this.getSourceFromElement(); : this.getSourceFromElement();
const is_async = source.includes('asyncio')
await this._register_esm(pyodide); this._register_esm(runtime);
if (is_async) { <string>await runtime.run(
await pyodide.runPythonAsync(
`output_manager.change(out="${this.outputElement.id}", err="${this.errorElement.id}", append=${this.appendOutput ? 'True' : 'False'})`, `output_manager.change(out="${this.outputElement.id}", err="${this.errorElement.id}", append=${this.appendOutput ? 'True' : 'False'})`,
); );
output = await pyodide.runPythonAsync(source); output = <string>await runtime.run(source);
} else {
output = pyodide.runPython(
`output_manager.change(out="${this.outputElement.id}", err="${this.errorElement.id}", append=${this.appendOutput ? 'True' : 'False'})`,
);
output = pyodide.runPython(source);
}
if (output !== undefined) { if (output !== undefined) {
if (Element === undefined) { if (Element === undefined) {
Element = pyodide.globals.get('Element'); Element = <Element>runtime.globals.get('Element');
} }
const out = Element(this.outputElement.id); const out = Element(this.outputElement.id);
out.write.callKwargs(output, { append: this.appendOutput }); out.write.callKwargs(output, { append: this.appendOutput });
@@ -156,61 +141,66 @@ export class BaseEvalElement extends HTMLElement {
this.outputElement.style.display = 'block'; this.outputElement.style.display = 'block';
} }
if (is_async) { await runtime.run(`output_manager.revert()`);
await pyodide.runPythonAsync(`output_manager.revert()`);
} else {
await pyodide.runPython(`output_manager.revert()`);
}
// check if this REPL contains errors, delete them and remove error classes // check if this REPL contains errors, delete them and remove error classes
const errorElements = document.querySelectorAll(`div[id^='${this.errorElement.id}'][error]`); const errorElements = document.querySelectorAll(`div[id^='${this.errorElement.id}'][error]`);
if (errorElements.length > 0) { if (errorElements.length > 0) {
for (const errorElement of errorElements) { errorElements.forEach( errorElement =>
{
errorElement.classList.add('hidden'); errorElement.classList.add('hidden');
if (this.hasAttribute('std-err')) { if (this.hasAttribute('std-err')) {
this.errorElement.hidden = true; this.errorElement.hidden = true;
this.errorElement.style.removeProperty('display'); this.errorElement.style.removeProperty('display');
} }
} }
)
} }
removeClasses(this.errorElement, ['bg-red-200', 'p-2']); removeClasses(this.errorElement, ['bg-red-200', 'p-2']);
this.postEvaluate(); this.postEvaluate();
} catch (err) { } catch (err) {
logger.error(err);
try{
if (Element === undefined) { if (Element === undefined) {
Element = pyodide.globals.get('Element'); Element = <Element>runtime.globals.get('Element');
} }
const out = Element(this.errorElement.id); const out = Element(this.errorElement.id);
addClasses(this.errorElement, ['bg-red-200', 'p-2']); addClasses(this.errorElement, ['bg-red-200', 'p-2']);
out.write.callKwargs(err, { append: this.appendOutput }); out.write.callKwargs(err.toString(), { append: this.appendOutput });
if (this.errorElement.children.length === 0){
this.errorElement.setAttribute('error', '');
}else{
this.errorElement.children[this.errorElement.children.length - 1].setAttribute('error', ''); this.errorElement.children[this.errorElement.children.length - 1].setAttribute('error', '');
}
this.errorElement.hidden = false; this.errorElement.hidden = false;
this.errorElement.style.display = 'block'; this.errorElement.style.display = 'block';
this.errorElement.style.visibility = 'visible'; this.errorElement.style.visibility = 'visible';
} catch (internalErr){
logger.error("Unnable to write error to error element in page.")
}
} }
} // end evaluate } // end evaluate
async eval(source: string): Promise<void> { async eval(source: string): Promise<void> {
let output;
const pyodide = runtime;
try { try {
output = await pyodide.runPythonAsync(source); const output = await runtime.run(source);
if (output !== undefined) { if (output !== undefined) {
console.log(output); logger.info(output);
} }
} catch (err) { } catch (err) {
console.log(err); logger.error(err);
} }
} // end eval } // end eval
runAfterRuntimeInitialized(callback: () => Promise<void>) { runAfterRuntimeInitialized(callback: () => Promise<void>) {
pyodideLoaded.subscribe(value => { runtimeLoaded.subscribe(value => {
if ('runPythonAsync' in value) { if ('run' in value) {
setTimeout(async () => { setTimeout(() => {
await callback(); void callback();
}, 100); }, 100);
} }
}); });
@@ -243,45 +233,44 @@ function createWidget(name: string, code: string, klass: string) {
// ideally we can just wait for it to load and then run. To do // ideally we can just wait for it to load and then run. To do
// so we need to replace using the promise and actually using // so we need to replace using the promise and actually using
// the interpreter after it loads completely // the interpreter after it loads completely
// setTimeout(async () => { // setTimeout(() => {
// void (async () => {
// await this.eval(this.code); // await this.eval(this.code);
// this.proxy = this.proxyClass(this); // this.proxy = this.proxyClass(this);
// console.log('proxy', this.proxy); // console.log('proxy', this.proxy);
// this.proxy.connect(); // this.proxy.connect();
// this.registerWidget(); // this.registerWidget();
// })();
// }, 2000); // }, 2000);
pyodideLoaded.subscribe(value => { runtimeLoaded.subscribe(value => {
console.log('RUNTIME READY', value); if ('run' in value) {
if ('runPythonAsync' in value) {
runtime = value; runtime = value;
setTimeout(async () => { setTimeout(() => {
void (async () => {
await this.eval(this.code); await this.eval(this.code);
this.proxy = this.proxyClass(this); this.proxy = this.proxyClass(this);
console.log('proxy', this.proxy);
this.proxy.connect(); this.proxy.connect();
this.registerWidget(); this.registerWidget();
})();
}, 1000); }, 1000);
} }
}); });
} }
registerWidget() { registerWidget() {
const pyodide = runtime; logger.info('new widget registered:', this.name);
console.log('new widget registered:', this.name); runtime.globals.set(this.id, this.proxy);
pyodide.globals.set(this.id, this.proxy);
} }
async eval(source: string): Promise<void> { async eval(source: string): Promise<void> {
let output;
const pyodide = runtime;
try { try {
output = await pyodide.runPythonAsync(source); const output = await runtime.run(source);
this.proxyClass = pyodide.globals.get(this.klass); this.proxyClass = runtime.globals.get(this.klass);
if (output !== undefined) { if (output !== undefined) {
console.log(output); logger.info('CustomWidget.eval: ', output);
} }
} catch (err) { } catch (err) {
console.log(err); logger.error('CustomWidget.eval: ', err);
} }
} }
} }
@@ -308,16 +297,15 @@ export class PyWidget extends HTMLElement {
this.wrapper = document.createElement('slot'); this.wrapper = document.createElement('slot');
this.shadow.appendChild(this.wrapper); this.shadow.appendChild(this.wrapper);
if (this.hasAttribute('src')) { this.addAttributes('src','name','klass');
this.source = this.getAttribute('src');
} }
if (this.hasAttribute('name')) { addAttributes(...attrs:string[]){
this.name = this.getAttribute('name'); for (const each of attrs){
const property = each === "src" ? "source" : each;
if (this.hasAttribute(each)) {
this[property]=this.getAttribute(each);
} }
if (this.hasAttribute('klass')) {
this.klass = this.getAttribute('klass');
} }
} }
@@ -331,7 +319,7 @@ export class PyWidget extends HTMLElement {
const mainDiv = document.createElement('div'); const mainDiv = document.createElement('div');
mainDiv.id = this.id + '-main'; mainDiv.id = this.id + '-main';
this.appendChild(mainDiv); this.appendChild(mainDiv);
console.log('reading source'); logger.debug('PyWidget: reading source', this.source);
this.code = await this.getSourceFromFile(this.source); this.code = await this.getSourceFromFile(this.source);
createWidget(this.name, this.code, this.klass); createWidget(this.name, this.code, this.klass);
} }
@@ -365,21 +353,18 @@ export class PyWidget extends HTMLElement {
} }
async getSourceFromFile(s: string): Promise<string> { async getSourceFromFile(s: string): Promise<string> {
const pyodide = runtime;
const response = await fetch(s); const response = await fetch(s);
return await response.text(); return await response.text();
} }
async eval(source: string): Promise<void> { async eval(source: string): Promise<void> {
let output;
const pyodide = runtime;
try { try {
output = await pyodide.runPythonAsync(source); const output = await runtime.run(source);
if (output !== undefined) { if (output !== undefined) {
console.log(output); logger.info('PyWidget.eval: ', output);
} }
} catch (err) { } catch (err) {
console.log(err); logger.error('PyWidget.eval: ', err);
} }
} }
} }

View File

@@ -0,0 +1,35 @@
import { PyRepl } from './pyrepl';
import { PyBox } from './pybox';
import { PyButton } from './pybutton';
import { PyTitle } from './pytitle';
import { PyInputBox } from './pyinputbox';
import { PyWidget } from './base';
/*
These were taken from main.js because some of our components call
runAfterRuntimeInitialized immediately when we are creating the custom
element, this was causing tests to fail since runAfterRuntimeInitialized
expects the runtime to have been loaded before being called.
This function is now called from within the `runtime.initialize`. Once
the runtime finished initializing, then we will create the custom elements
so they are rendered in the page and we will always have a runtime available.
Ideally, this would live under utils.js, but importing all the components in
the utils.js file was causing jest to fail with weird errors such as:
"ReferenceError: Cannot access 'BaseEvalElement' before initialization" coming
from the PyScript class.
*/
function createCustomElements() {
/* eslint-disable @typescript-eslint/no-unused-vars */
const xPyRepl = customElements.define('py-repl', PyRepl);
const xPyBox = customElements.define('py-box', PyBox);
const xPyTitle = customElements.define('py-title', PyTitle);
const xPyWidget = customElements.define('py-register-widget', PyWidget);
const xPyInputBox = customElements.define('py-inputbox', PyInputBox);
const xPyButton = customElements.define('py-button', PyButton);
/* eslint-enable @typescript-eslint/no-unused-vars */
}
export { createCustomElements };

View File

@@ -1,4 +1,7 @@
import { addClasses } from '../utils'; import { addClasses } from '../utils';
import { getLogger } from '../logger';
const logger = getLogger('py-box');
export class PyBox extends HTMLElement { export class PyBox extends HTMLElement {
shadow: ShadowRoot; shadow: ShadowRoot;
@@ -18,13 +21,12 @@ export class PyBox extends HTMLElement {
connectedCallback() { connectedCallback() {
const mainDiv = document.createElement('div'); const mainDiv = document.createElement('div');
addClasses(mainDiv, ['flex', 'mx-8']); addClasses(mainDiv, ['py-box']);
// Hack: for some reason when moving children, the editor box duplicates children // Hack: for some reason when moving children, the editor box duplicates children
// meaning that we end up with 2 editors, if there's a <py-repl> inside the <py-box> // meaning that we end up with 2 editors, if there's a <py-repl> inside the <py-box>
// so, if we have more than 2 children with the cm-editor class, we remove one of them // so, if we have more than 2 children with the cm-editor class, we remove one of them
while (this.childNodes.length > 0) { while (this.childNodes.length > 0) {
console.log(this.firstChild);
if (this.firstChild.nodeName == 'PY-REPL') { if (this.firstChild.nodeName == 'PY-REPL') {
// in this case we need to remove the child and create a new one from scratch // in this case we need to remove the child and create a new one from scratch
const replDiv = document.createElement('div'); const replDiv = document.createElement('div');
@@ -44,20 +46,23 @@ export class PyBox extends HTMLElement {
// now we need to set widths // now we need to set widths
this.widths = []; this.widths = [];
if (this.hasAttribute('widths')) { if (this.hasAttribute('widths')) {
for (const w of this.getAttribute('widths').split(';')) { for (const w of this.getAttribute('widths').split(';')) {
this.widths.push(`w-${w}`); if (w.includes('/')) this.widths.push(w.split('/')[0])
else this.widths.push(w)
} }
} else { } else {
this.widths = [...this.widths, ...[`w-1/${mainDiv.childNodes.length}`]]; this.widths = Array<string>(mainDiv.children.length).fill('1 1 0');
} }
this.widths.forEach((width, index) => { this.widths.forEach((width, index) => {
const node: ChildNode = mainDiv.childNodes[index]; const node: ChildNode = mainDiv.childNodes[index];
addClasses(node as HTMLElement, [width, 'mx-1']); (<HTMLElement>node).style.flex = width;
addClasses((<HTMLElement>node), ['py-box-child']);
}); });
this.appendChild(mainDiv); this.appendChild(mainDiv);
console.log('py-box connected'); logger.info('py-box connected');
} }
} }

View File

@@ -1,10 +1,11 @@
import { BaseEvalElement } from './base'; import { BaseEvalElement } from './base';
import { addClasses, htmlDecode } from '../utils'; import { addClasses, htmlDecode } from '../utils';
import { getLogger } from '../logger'
const logger = getLogger('py-button');
export class PyButton extends BaseEvalElement { export class PyButton extends BaseEvalElement {
shadow: ShadowRoot;
wrapper: HTMLElement;
theme: string;
widths: Array<string>; widths: Array<string>;
label: string; label: string;
class: Array<string>; class: Array<string>;
@@ -13,7 +14,7 @@ export class PyButton extends BaseEvalElement {
constructor() { constructor() {
super(); super();
this.defaultClass = ['p-2', 'text-white', 'bg-blue-600', 'border', 'border-blue-600', 'rounded']; this.defaultClass = ['py-button'];
if (this.hasAttribute('label')) { if (this.hasAttribute('label')) {
this.label = this.getAttribute('label'); this.label = this.getAttribute('label');
@@ -51,7 +52,7 @@ export class PyButton extends BaseEvalElement {
this.appendChild(mainDiv); this.appendChild(mainDiv);
this.code = this.code.split('self').join(this.mount_name); this.code = this.code.split('self').join(this.mount_name);
let registrationCode = `from pyodide import create_proxy`; let registrationCode = `from pyodide.ffi import create_proxy`;
registrationCode += `\n${this.mount_name} = Element("${mainDiv.id}")`; registrationCode += `\n${this.mount_name} = Element("${mainDiv.id}")`;
if (this.code.includes('def on_focus')) { if (this.code.includes('def on_focus')) {
this.code = this.code.replace('def on_focus', `def on_focus_${this.mount_name}`); this.code = this.code.replace('def on_focus', `def on_focus_${this.mount_name}`);
@@ -68,9 +69,9 @@ export class PyButton extends BaseEvalElement {
this.runAfterRuntimeInitialized(async () => { this.runAfterRuntimeInitialized(async () => {
await this.eval(this.code); await this.eval(this.code);
await this.eval(registrationCode); await this.eval(registrationCode);
console.log('registered handlers'); logger.debug('registered handlers');
}); });
console.log('py-button connected'); logger.debug('py-button connected');
} }
} }

View File

@@ -1,176 +1,86 @@
import * as jsyaml from 'js-yaml';
import { BaseEvalElement } from './base'; import { BaseEvalElement } from './base';
import { import { appConfig, addInitializer, runtimeLoaded } from '../stores';
initializers, import type { AppConfig, Runtime } from '../runtime';
loadedEnvironments, import { version } from '../runtime';
mode, import { PyodideRuntime } from '../pyodide';
postInitializers, import { getLogger } from '../logger';
pyodideLoaded, import { readTextFromPath, handleFetchError, mergeConfig, validateConfig, defaultConfig } from '../utils'
scriptsQueue,
globalLoader,
appConfig,
Initializer,
} from '../stores';
import { loadInterpreter } from '../interpreter';
import type { PyLoader } from './pyloader';
import type { PyScript } from './pyscript';
const DEFAULT_RUNTIME = { // Subscriber used to connect to the first available runtime (can be pyodide or others)
src: 'https://cdn.jsdelivr.net/pyodide/v0.20.0/full/pyodide.js', let runtimeSpec: Runtime;
name: 'pyodide-default', runtimeLoaded.subscribe(value => {
lang: 'python', runtimeSpec = value;
}; });
export type Runtime = { let appConfig_: AppConfig;
src: string; appConfig.subscribe(value => {
name?: string;
lang?: string;
};
export type AppConfig = {
autoclose_loader: boolean;
name?: string;
version?: string;
runtimes?: Array<Runtime>;
};
let appConfig_: AppConfig = {
autoclose_loader: true,
};
appConfig.subscribe((value: AppConfig) => {
if (value) {
appConfig_ = value; appConfig_ = value;
}
console.log('config set!');
}); });
let initializers_: Initializer[]; const logger = getLogger('py-config');
initializers.subscribe((value: Initializer[]) => {
initializers_ = value;
console.log('initializers set');
});
let postInitializers_: Initializer[]; /**
postInitializers.subscribe((value: Initializer[]) => { * Configures general metadata about the PyScript application such
postInitializers_ = value; * as a list of runtimes, name, version, closing the loader
console.log('post initializers set'); * automatically, etc.
}); *
* Also initializes the different runtimes passed. If no runtime is passed,
let scriptsQueue_: PyScript[]; * the default runtime based on Pyodide is used.
scriptsQueue.subscribe((value: PyScript[]) => { */
scriptsQueue_ = value;
console.log('post initializers set');
});
let mode_: string;
mode.subscribe((value: string) => {
mode_ = value;
console.log('post initializers set');
});
let pyodideReadyPromise;
let loader: PyLoader | undefined;
globalLoader.subscribe(value => {
loader = value;
});
export class PyodideRuntime extends Object {
src: string;
constructor(url: string) {
super();
this.src = url;
}
async initialize() {
loader?.log('Loading runtime...');
pyodideReadyPromise = loadInterpreter(this.src);
const pyodide = await pyodideReadyPromise;
const newEnv = {
id: 'a',
promise: pyodideReadyPromise,
runtime: pyodide,
state: 'loading',
};
pyodideLoaded.set(pyodide);
// Inject the loader into the runtime namespace
// eslint-disable-next-line
pyodide.globals.set('pyscript_loader', loader);
loader?.log('Runtime created...');
loadedEnvironments.update((value: any): any => {
value[newEnv['id']] = newEnv;
});
// now we call all initializers before we actually executed all page scripts
loader?.log('Initializing components...');
for (const initializer of initializers_) {
await initializer();
}
// now we can actually execute the page scripts if we are in play mode
loader?.log('Initializing scripts...');
if (mode_ == 'play') {
for (const script of scriptsQueue_) {
script.evaluate();
}
scriptsQueue.set([]);
}
// now we call all post initializers AFTER we actually executed all page scripts
loader?.log('Running post initializers...');
if (appConfig_ && appConfig_.autoclose_loader) {
loader?.close();
console.log('------ loader closed ------');
}
setTimeout(() => {
for (const initializer of postInitializers_) {
initializer();
}
}, 3000);
}
}
export class PyConfig extends BaseEvalElement { export class PyConfig extends BaseEvalElement {
shadow: ShadowRoot;
wrapper: HTMLElement;
theme: string;
widths: Array<string>; widths: Array<string>;
label: string; label: string;
mount_name: string; mount_name: string;
details: HTMLElement; details: HTMLElement;
operation: HTMLElement; operation: HTMLElement;
code: string;
values: AppConfig; values: AppConfig;
constructor() { constructor() {
super(); super();
} }
connectedCallback() { extractFromSrc(configType: string) {
if (this.hasAttribute('src'))
{
logger.info('config set from src attribute');
return validateConfig(readTextFromPath(this.getAttribute('src')), configType);
}
return {};
}
extractFromInline(configType: string) {
if (this.innerHTML!=='')
{
this.code = this.innerHTML; this.code = this.innerHTML;
this.innerHTML = ''; this.innerHTML = '';
logger.info('config set from inline');
return validateConfig(this.code, configType);
}
return {};
}
const loadedValues = jsyaml.load(this.code); injectMetadata() {
if (loadedValues === undefined) { this.values.pyscript = {
this.values = { "version": version,
autoclose_loader: true, "time": new Date().toISOString()
}; };
} else {
// eslint-disable-next-line
// @ts-ignore
this.values = loadedValues;
} }
if (this.values.runtimes === undefined) {
this.values.runtimes = [DEFAULT_RUNTIME];
}
appConfig.set(this.values);
console.log('config set', this.values);
connectedCallback() {
const configType: string = this.hasAttribute("type") ? this.getAttribute("type") : "toml";
let srcConfig = this.extractFromSrc(configType);
const inlineConfig = this.extractFromInline(configType);
// first make config from src whole if it is partial
srcConfig = mergeConfig(srcConfig, defaultConfig);
// then merge inline config and config from src
this.values = mergeConfig(inlineConfig, srcConfig);
this.injectMetadata();
appConfig.set(this.values);
logger.info('config set:', this.values);
addInitializer(this.loadPackages);
addInitializer(this.loadPaths);
this.loadRuntimes(); this.loadRuntimes();
} }
@@ -184,14 +94,35 @@ export class PyConfig extends BaseEvalElement {
this.remove(); this.remove();
} }
loadPackages = async () => {
const env = appConfig_.packages;
logger.info("Loading env: ", env);
await runtimeSpec.installPackage(env);
}
loadPaths = async () => {
const paths = appConfig_.paths;
logger.info("Paths to load: ", paths)
for (const singleFile of paths) {
logger.info(` loading path: ${singleFile}`);
try {
await runtimeSpec.loadFromFile(singleFile);
} catch (e) {
//Should we still export full error contents to console?
handleFetchError(<Error>e, singleFile);
}
}
logger.info("All paths loaded");
}
loadRuntimes() { loadRuntimes() {
console.log('Initializing runtimes...'); logger.info('Initializing runtimes');
for (const runtime of this.values.runtimes) { for (const runtime of this.values.runtimes) {
const runtimeObj: Runtime = new PyodideRuntime(runtime.src, runtime.name, runtime.lang);
const script = document.createElement('script'); // create a script DOM node const script = document.createElement('script'); // create a script DOM node
const runtimeSpec = new PyodideRuntime(runtime.src); script.src = runtimeObj.src; // set its src to the provided URL
script.src = runtime.src; // set its src to the provided URL
script.addEventListener('load', () => { script.addEventListener('load', () => {
void runtimeSpec.initialize(); void runtimeObj.initialize();
}); });
document.head.appendChild(script); document.head.appendChild(script);
} }

View File

@@ -1,16 +1,17 @@
import * as jsyaml from 'js-yaml'; import * as jsyaml from 'js-yaml';
import { pyodideLoaded, addInitializer } from '../stores'; import { runtimeLoaded, addInitializer } from '../stores';
import { loadPackage, loadFromFile } from '../interpreter';
import { handleFetchError } from '../utils'; import { handleFetchError } from '../utils';
import type { Runtime } from '../runtime';
import { getLogger } from '../logger';
// Premise used to connect to the first available pyodide interpreter const logger = getLogger('py-env');
let pyodideReadyPromise;
let runtime;
pyodideLoaded.subscribe(value => { // Premise used to connect to the first available runtime (can be pyodide or others)
let runtime: Runtime;
runtimeLoaded.subscribe(value => {
runtime = value; runtime = value;
console.log('RUNTIME READY');
}); });
export class PyEnv extends HTMLElement { export class PyEnv extends HTMLElement {
@@ -18,7 +19,7 @@ export class PyEnv extends HTMLElement {
wrapper: HTMLElement; wrapper: HTMLElement;
code: string; code: string;
environment: unknown; environment: unknown;
runtime: any; runtime: Runtime;
env: string[]; env: string[];
paths: string[]; paths: string[];
@@ -30,6 +31,7 @@ export class PyEnv extends HTMLElement {
} }
connectedCallback() { connectedCallback() {
logger.info("The <py-env> tag is deprecated, please use <py-config> instead.")
this.code = this.innerHTML; this.code = this.innerHTML;
this.innerHTML = ''; this.innerHTML = '';
@@ -56,25 +58,25 @@ export class PyEnv extends HTMLElement {
this.paths = paths; this.paths = paths;
async function loadEnv() { async function loadEnv() {
await loadPackage(env, runtime); logger.info("Loading env: ", env);
console.log('environment loaded'); await runtime.installPackage(env);
} }
async function loadPaths() { async function loadPaths() {
logger.info("Paths to load: ", paths)
for (const singleFile of paths) { for (const singleFile of paths) {
console.log(`loading ${singleFile}`); logger.info(` loading path: ${singleFile}`);
try { try {
await loadFromFile(singleFile, runtime); await runtime.loadFromFile(singleFile);
} catch (e) { } catch (e) {
//Should we still export full error contents to console? //Should we still export full error contents to console?
handleFetchError(e, singleFile); handleFetchError(<Error>e, singleFile);
} }
} }
console.log('paths loaded'); logger.info("All paths loaded");
} }
addInitializer(loadEnv); addInitializer(loadEnv);
addInitializer(loadPaths); addInitializer(loadPaths);
console.log('environment loading...', this.env);
} }
} }

View File

@@ -1,10 +1,10 @@
import { BaseEvalElement } from './base'; import { BaseEvalElement } from './base';
import { addClasses, htmlDecode } from '../utils'; import { addClasses, htmlDecode } from '../utils';
import { getLogger } from '../logger'
const logger = getLogger('py-inputbox');
export class PyInputBox extends BaseEvalElement { export class PyInputBox extends BaseEvalElement {
shadow: ShadowRoot;
wrapper: HTMLElement;
theme: string;
widths: Array<string>; widths: Array<string>;
label: string; label: string;
mount_name: string; mount_name: string;
@@ -24,7 +24,7 @@ export class PyInputBox extends BaseEvalElement {
const mainDiv = document.createElement('input'); const mainDiv = document.createElement('input');
mainDiv.type = 'text'; mainDiv.type = 'text';
addClasses(mainDiv, ['border', 'flex-1', 'w-full', 'mr-3', 'border-gray-300', 'p-2', 'rounded']); addClasses(mainDiv, ['py-input']);
mainDiv.id = this.id; mainDiv.id = this.id;
this.id = `${this.id}-container`; this.id = `${this.id}-container`;
@@ -34,7 +34,7 @@ export class PyInputBox extends BaseEvalElement {
// defined for this widget // defined for this widget
this.appendChild(mainDiv); this.appendChild(mainDiv);
this.code = this.code.split('self').join(this.mount_name); this.code = this.code.split('self').join(this.mount_name);
let registrationCode = `from pyodide import create_proxy`; let registrationCode = `from pyodide.ffi import create_proxy`;
registrationCode += `\n${this.mount_name} = Element("${mainDiv.id}")`; registrationCode += `\n${this.mount_name} = Element("${mainDiv.id}")`;
if (this.code.includes('def on_keypress')) { if (this.code.includes('def on_keypress')) {
this.code = this.code.replace('def on_keypress', `def on_keypress_${this.mount_name}`); this.code = this.code.replace('def on_keypress', `def on_keypress_${this.mount_name}`);
@@ -46,7 +46,7 @@ export class PyInputBox extends BaseEvalElement {
this.runAfterRuntimeInitialized(async () => { this.runAfterRuntimeInitialized(async () => {
await this.eval(this.code); await this.eval(this.code);
await this.eval(registrationCode); await this.eval(registrationCode);
console.log('registered handlers'); logger.debug('registered handlers');
}); });
} }
} }

View File

@@ -1,9 +1,9 @@
import { BaseEvalElement } from './base'; import { BaseEvalElement } from './base';
import { getLogger } from '../logger';
const logger = getLogger('py-loader');
export class PyLoader extends BaseEvalElement { export class PyLoader extends BaseEvalElement {
shadow: ShadowRoot;
wrapper: HTMLElement;
theme: string;
widths: Array<string>; widths: Array<string>;
label: string; label: string;
mount_name: string; mount_name: string;
@@ -14,12 +14,14 @@ export class PyLoader extends BaseEvalElement {
} }
connectedCallback() { connectedCallback() {
this.innerHTML = `<div id="pyscript_loading_splash" class="fixed top-0 left-0 right-0 bottom-0 w-full h-screen z-50 overflow-hidden bg-gray-600 opacity-75 flex flex-col items-center justify-center"> this.innerHTML = `<div id="pyscript_loading_splash" class="py-overlay">
<div class="py-pop-up">
<div class="smooth spinner"></div> <div class="smooth spinner"></div>
<div id="pyscript-loading-label" class="label"> <div id="pyscript-loading-label" class="label">
<div id="pyscript-operation-details"> <div id="pyscript-operation-details">
</div> </div>
</div> </div>
</div>
</div>`; </div>`;
this.mount_name = this.id.split('-').join('_'); this.mount_name = this.id.split('-').join('_');
this.operation = document.getElementById('pyscript-operation'); this.operation = document.getElementById('pyscript-operation');
@@ -27,12 +29,15 @@ export class PyLoader extends BaseEvalElement {
} }
log(msg: string) { log(msg: string) {
// loader messages are showed both in the HTML and in the console
logger.info(msg);
const newLog = document.createElement('p'); const newLog = document.createElement('p');
newLog.innerText = msg; newLog.innerText = msg;
this.details.appendChild(newLog); this.details.appendChild(newLog);
} }
close() { close() {
logger.info('Closing');
this.remove(); this.remove();
} }
} }

View File

@@ -4,33 +4,12 @@ import { Compartment, StateCommand } from '@codemirror/state';
import { keymap } from '@codemirror/view'; import { keymap } from '@codemirror/view';
import { defaultKeymap } from '@codemirror/commands'; import { defaultKeymap } from '@codemirror/commands';
import { oneDarkTheme } from '@codemirror/theme-one-dark'; import { oneDarkTheme } from '@codemirror/theme-one-dark';
import { componentDetailsNavOpen, loadedEnvironments, mode, pyodideLoaded } from '../stores';
import { addClasses, htmlDecode } from '../utils'; import { addClasses, htmlDecode } from '../utils';
import { BaseEvalElement } from './base'; import { BaseEvalElement } from './base';
import { getLogger } from '../logger';
// Premise used to connect to the first available pyodide interpreter const logger = getLogger('py-repl');
let pyodideReadyPromise;
let environments;
let currentMode;
pyodideLoaded.subscribe(value => {
pyodideReadyPromise = value;
});
loadedEnvironments.subscribe(value => {
environments = value;
});
let propertiesNavOpen;
componentDetailsNavOpen.subscribe(value => {
propertiesNavOpen = value;
});
mode.subscribe(value => {
currentMode = value;
});
function createCmdHandler(el: PyRepl): StateCommand { function createCmdHandler(el: PyRepl): StateCommand {
// Creates a codemirror cmd handler that calls the el.evaluate when an event // Creates a codemirror cmd handler that calls the el.evaluate when an event
@@ -55,7 +34,7 @@ export class PyRepl extends BaseEvalElement {
// add an extra div where we can attach the codemirror editor // add an extra div where we can attach the codemirror editor
this.editorNode = document.createElement('div'); this.editorNode = document.createElement('div');
addClasses(this.editorNode, ['editor-box', 'border', 'border-gray-300', 'group', 'relative']); addClasses(this.editorNode, ['editor-box']);
this.shadow.appendChild(this.wrapper); this.shadow.appendChild(this.wrapper);
} }
@@ -88,7 +67,7 @@ export class PyRepl extends BaseEvalElement {
}); });
const mainDiv = document.createElement('div'); const mainDiv = document.createElement('div');
addClasses(mainDiv, ['parentBox', 'flex', 'flex-col', 'mt-2', 'mx-8', 'relative']); addClasses(mainDiv, ['py-repl-box']);
// Styles that we use to hide the labels whilst also keeping it accessible for screen readers // Styles that we use to hide the labels whilst also keeping it accessible for screen readers
const labelStyle = 'overflow:hidden; display:block; width:1px; height:1px'; const labelStyle = 'overflow:hidden; display:block; width:1px; height:1px';
@@ -110,7 +89,7 @@ export class PyRepl extends BaseEvalElement {
this.btnRun.id = 'btnRun'; this.btnRun.id = 'btnRun';
this.btnRun.innerHTML = this.btnRun.innerHTML =
'<svg id="" class="svelte-fa svelte-ps5qeg" 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>'; '<svg id="" class="svelte-fa svelte-ps5qeg" 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>';
addClasses(this.btnRun, ['absolute', 'right-1', 'bottom-1', 'opacity-0', 'group-hover:opacity-100']); addClasses(this.btnRun, ['absolute', 'repl-play-button']);
// Play Button Label // Play Button Label
const btnLabel = document.createElement('label'); const btnLabel = document.createElement('label');
@@ -126,8 +105,8 @@ export class PyRepl extends BaseEvalElement {
}); });
if (!this.id) { if (!this.id) {
console.log( logger.warn(
"WARNING: <pyrepl> define with an id. <pyrepl> should always have an id. More than one <pyrepl> on a page won't work otherwise!", "WARNING: <py-repl> defined without an id. <py-repl> should always have an id, otherwise multiple <py-repl> in the same page will not work!"
); );
} }
@@ -148,7 +127,7 @@ export class PyRepl extends BaseEvalElement {
// In this case neither output or std-out have been provided so we need // In this case neither output or std-out have been provided so we need
// to create a new output div to output to // to create a new output div to output to
this.outputElement = document.createElement('div'); this.outputElement = document.createElement('div');
this.outputElement.classList.add('output', 'font-mono', 'ml-8', 'text-sm'); this.outputElement.classList.add('output');
this.outputElement.hidden = true; this.outputElement.hidden = true;
this.outputElement.id = this.id + '-' + this.getAttribute('exec-id'); this.outputElement.id = this.id + '-' + this.getAttribute('exec-id');
@@ -163,7 +142,7 @@ export class PyRepl extends BaseEvalElement {
this.appendChild(mainDiv); this.appendChild(mainDiv);
this.editor.focus(); this.editor.focus();
console.log('connected'); logger.debug(`element ${this.id} successfully connected`);
} }
addToOutput(s: string): void { addToOutput(s: string): void {
@@ -217,15 +196,6 @@ export class PyRepl extends BaseEvalElement {
} }
getSourceFromElement(): string { getSourceFromElement(): string {
const sourceStrings = [ return this.editor.state.doc.toString();
`output_manager.change(out="${this.outputElement.id}", append=True)`,
...this.editor.state.doc.toString().split('\n'),
];
return sourceStrings.join('\n');
}
render() {
console.log('rendered');
} }
} }

View File

@@ -3,33 +3,28 @@ import {
addPostInitializer, addPostInitializer,
addToScriptsQueue, addToScriptsQueue,
loadedEnvironments, loadedEnvironments,
mode, runtimeLoaded,
pyodideLoaded, type Environment,
} from '../stores'; } from '../stores';
import { addClasses, htmlDecode } from '../utils'; import { addClasses, htmlDecode } from '../utils';
import { BaseEvalElement } from './base'; import { BaseEvalElement } from './base';
import type { Runtime } from '../runtime';
import { getLogger } from '../logger';
// Premise used to connect to the first available pyodide interpreter const logger = getLogger('py-script');
let pyodideReadyPromise;
let environments;
let currentMode;
pyodideLoaded.subscribe(value => { // Premise used to connect to the first available runtime (can be pyodide or others)
pyodideReadyPromise = value; let runtime: Runtime;
let environments: Record<Environment['id'], Environment> = {};
runtimeLoaded.subscribe(value => {
runtime = value;
}); });
loadedEnvironments.subscribe(value => { loadedEnvironments.subscribe(value => {
environments = value; environments = value;
}); });
mode.subscribe(value => {
currentMode = value;
});
// TODO: use type declaractions
type PyodideInterface = {
registerJsModule(name: string, module: object): void;
};
export class PyScript extends BaseEvalElement { export class PyScript extends BaseEvalElement {
constructor() { constructor() {
super(); super();
@@ -44,7 +39,7 @@ export class PyScript extends BaseEvalElement {
this.innerHTML = ''; this.innerHTML = '';
const mainDiv = document.createElement('div'); const mainDiv = document.createElement('div');
addClasses(mainDiv, ['parentBox', 'flex', 'flex-col', 'mx-8']); addClasses(mainDiv, ['output']);
// add Editor to main PyScript div // add Editor to main PyScript div
if (this.hasAttribute('output')) { if (this.hasAttribute('output')) {
@@ -77,22 +72,15 @@ export class PyScript extends BaseEvalElement {
} }
} }
if (currentMode == 'edit') {
// TODO: We need to build a plan for this
this.appendChild(mainDiv);
} else {
this.appendChild(mainDiv); this.appendChild(mainDiv);
addToScriptsQueue(this); addToScriptsQueue(this);
}
console.log('connected');
if (this.hasAttribute('src')) { if (this.hasAttribute('src')) {
this.source = this.getAttribute('src'); this.source = this.getAttribute('src');
} }
} }
protected async _register_esm(pyodide: PyodideInterface): Promise<void> { protected async _register_esm(runtime: Runtime): Promise<void> {
for (const node of document.querySelectorAll("script[type='importmap']")) { for (const node of document.querySelectorAll("script[type='importmap']")) {
const importmap = (() => { const importmap = (() => {
try { try {
@@ -113,11 +101,11 @@ export class PyScript extends BaseEvalElement {
// "can't read 'name' of undefined" at import time // "can't read 'name' of undefined" at import time
exports = { ...(await import(url)) }; exports = { ...(await import(url)) };
} catch { } catch {
console.warn(`failed to fetch '${url}' for '${name}'`); logger.warn(`failed to fetch '${url}' for '${name}'`);
continue; continue;
} }
pyodide.registerJsModule(name, exports); runtime.registerJsModule(name, exports);
} }
} }
} }
@@ -127,36 +115,132 @@ export class PyScript extends BaseEvalElement {
} }
} }
/** Defines all possible pys-on* and their corresponding event types */ /** Defines all possible py-on* and their corresponding event types */
const pysAttributeToEvent: Map<string, string> = new Map<string, string>([ const pyAttributeToEvent: Map<string, string> = new Map<string, string>([
// Leaving pys-onClick and pys-onKeyDown for backward compatibility
["pys-onClick", "click"], ["pys-onClick", "click"],
["pys-onKeyDown", "keydown"] ["pys-onKeyDown", "keydown"],
["py-onClick", "click"],
["py-onKeyDown", "keydown"],
// Window Events
["py-afterprint", "afterprint"],
["py-beforeprint", "beforeprint"],
["py-beforeunload", "beforeunload"],
["py-error", "error"],
["py-hashchange", "hashchange"],
["py-load", "load"],
["py-message", "message"],
["py-offline", "offline"],
["py-online", "online"],
["py-pagehide", "pagehide"],
["py-pageshow", "pageshow"],
["py-popstate", "popstate"],
["py-resize", "resize"],
["py-storage", "storage"],
["py-unload", "unload"],
// Form Events
["py-blur", "blur"],
["py-change", "change"],
["py-contextmenu", "contextmenu"],
["py-focus", "focus"],
["py-input", "input"],
["py-invalid", "invalid"],
["py-reset", "reset"],
["py-search", "search"],
["py-select", "select"],
["py-submit", "submit"],
// Keyboard Events
["py-keydown", "keydown"],
["py-keypress", "keypress"],
["py-keyup", "keyup"],
// Mouse Events
["py-click", "click"],
["py-dblclick", "dblclick"],
["py-mousedown", "mousedown"],
["py-mousemove", "mousemove"],
["py-mouseout", "mouseout"],
["py-mouseover", "mouseover"],
["py-mouseup", "mouseup"],
["py-mousewheel", "mousewheel"],
["py-wheel", "wheel"],
// Drag Events
["py-drag", "drag"],
["py-dragend", "dragend"],
["py-dragenter", "dragenter"],
["py-dragleave", "dragleave"],
["py-dragover", "dragover"],
["py-dragstart", "dragstart"],
["py-drop", "drop"],
["py-scroll", "scroll"],
// Clipboard Events
["py-copy", "copy"],
["py-cut", "cut"],
["py-paste", "paste"],
// Media Events
["py-abort", "abort"],
["py-canplay", "canplay"],
["py-canplaythrough", "canplaythrough"],
["py-cuechange", "cuechange"],
["py-durationchange", "durationchange"],
["py-emptied", "emptied"],
["py-ended", "ended"],
["py-loadeddata", "loadeddata"],
["py-loadedmetadata", "loadedmetadata"],
["py-loadstart", "loadstart"],
["py-pause", "pause"],
["py-play", "play"],
["py-playing", "playing"],
["py-progress", "progress"],
["py-ratechange", "ratechange"],
["py-seeked", "seeked"],
["py-seeking", "seeking"],
["py-stalled", "stalled"],
["py-suspend", "suspend"],
["py-timeupdate", "timeupdate"],
["py-volumechange", "volumechange"],
["py-waiting", "waiting"],
// Misc Events
["py-toggle", "toggle"],
]); ]);
/** Initialize all elements with pys-on* handlers attributes */ /** Initialize all elements with py-* handlers attributes */
async function initHandlers() { async function initHandlers() {
console.log('Collecting nodes...'); logger.debug('Initializing py-* event handlers...');
const pyodide = await pyodideReadyPromise; for (const pyAttribute of pyAttributeToEvent.keys()) {
for (const pysAttribute of pysAttributeToEvent.keys()) { await createElementsWithEventListeners(runtime, pyAttribute);
await createElementsWithEventListeners(pyodide, pysAttribute);
} }
} }
/** Initializes an element with the given pys-on* attribute and its handler */ /** Initializes an element with the given py-on* attribute and its handler */
async function createElementsWithEventListeners(pyodide: any, pysAttribute: string) { async function createElementsWithEventListeners(runtime: Runtime, pyAttribute: string): Promise<void> {
const matches: NodeListOf<HTMLElement> = document.querySelectorAll(`[${pysAttribute}]`); const matches: NodeListOf<HTMLElement> = document.querySelectorAll(`[${pyAttribute}]`);
for (const el of matches) { for (const el of matches) {
if (el.id.length === 0) { if (el.id.length === 0) {
throw new TypeError(`<${el.tagName.toLowerCase()}> must have an id attribute, when using the ${pysAttribute} attribute`) throw new TypeError(`<${el.tagName.toLowerCase()}> must have an id attribute, when using the ${pyAttribute} attribute`)
} }
const handlerCode = el.getAttribute(pysAttribute); const handlerCode = el.getAttribute(pyAttribute);
const event = pysAttributeToEvent.get(pysAttribute); const event = pyAttributeToEvent.get(pyAttribute);
if (pyAttribute === 'pys-onClick' || pyAttribute === 'pys-onKeyDown'){
console.warn("Use of pys-onClick and pys-onKeyDown attributes is deprecated in favor of py-onClick() and py-onKeyDown(). pys-on* attributes will be deprecated in a future version of PyScript.")
const source = ` const source = `
from pyodide import create_proxy from pyodide.ffi import create_proxy
Element("${el.id}").element.addEventListener("${event}", create_proxy(${handlerCode})) Element("${el.id}").element.addEventListener("${event}", create_proxy(${handlerCode}))
`; `;
await pyodide.runPythonAsync(source); await runtime.run(source);
}
else{
el.addEventListener(event, () => {
(async() => {await runtime.run(handlerCode)})();
});
}
// TODO: Should we actually map handlers in JS instead of Python? // TODO: Should we actually map handlers in JS instead of Python?
// el.onclick = (evt: any) => { // el.onclick = (evt: any) => {
// console.log("click"); // console.log("click");
@@ -167,7 +251,7 @@ async function createElementsWithEventListeners(pyodide: any, pysAttribute: stri
// }).then(() => { // }).then(() => {
// console.log("resolved") // console.log("resolved")
// }); // });
// // let handlerCode = el.getAttribute('pys-onClick'); // // let handlerCode = el.getAttribute('py-onClick');
// // pyodide.runPython(handlerCode); // // pyodide.runPython(handlerCode);
// } // }
} }
@@ -176,16 +260,15 @@ async function createElementsWithEventListeners(pyodide: any, pysAttribute: stri
/** Mount all elements with attribute py-mount into the Python namespace */ /** Mount all elements with attribute py-mount into the Python namespace */
async function mountElements() { async function mountElements() {
console.log('Collecting nodes to be mounted into python namespace...');
const pyodide = await pyodideReadyPromise;
const matches: NodeListOf<HTMLElement> = document.querySelectorAll('[py-mount]'); const matches: NodeListOf<HTMLElement> = document.querySelectorAll('[py-mount]');
logger.info(`py-mount: found ${matches.length} elements`);
let source = ''; let source = '';
for (const el of matches) { for (const el of matches) {
const mountName = el.getAttribute('py-mount') || el.id.split('-').join('_'); const mountName = el.getAttribute('py-mount') || el.id.split('-').join('_');
source += `\n${mountName} = Element("${el.id}")`; source += `\n${mountName} = Element("${el.id}")`;
} }
await pyodide.runPythonAsync(source); await runtime.run(source);
} }
addInitializer(mountElements); addInitializer(mountElements);
addPostInitializer(initHandlers); addPostInitializer(initHandlers);

View File

@@ -2,9 +2,6 @@ import { BaseEvalElement } from './base';
import { addClasses, htmlDecode } from '../utils'; import { addClasses, htmlDecode } from '../utils';
export class PyTitle extends BaseEvalElement { export class PyTitle extends BaseEvalElement {
shadow: ShadowRoot;
wrapper: HTMLElement;
theme: string;
widths: Array<string>; widths: Array<string>;
label: string; label: string;
mount_name: string; mount_name: string;
@@ -20,8 +17,7 @@ export class PyTitle extends BaseEvalElement {
const mainDiv = document.createElement('div'); const mainDiv = document.createElement('div');
const divContent = document.createElement('h1'); const divContent = document.createElement('h1');
addClasses(mainDiv, ['text-center', 'w-full', 'mb-8']); addClasses(mainDiv, ['py-title']);
addClasses(divContent, ['text-3xl', 'font-bold', 'text-gray-800', 'uppercase', 'tracking-tight']);
divContent.innerHTML = this.label; divContent.innerHTML = this.label;
mainDiv.id = this.id; mainDiv.id = this.id;

View File

@@ -1,62 +0,0 @@
import { getLastPath } from './utils';
// eslint-disable-next-line
// @ts-ignore
import pyscript from './pyscript.py';
let pyodideReadyPromise;
let pyodide;
const loadInterpreter = async function (indexUrl: string): Promise<any> {
console.log('creating pyodide runtime');
// eslint-disable-next-line
// @ts-ignore
pyodide = await loadPyodide({
// indexURL: indexUrl,
stdout: console.log,
stderr: console.log,
fullStdLib: false,
});
// now that we loaded, add additional convenience functions
console.log('loading micropip');
await pyodide.loadPackage('micropip');
console.log('loading pyscript...');
await pyodide.runPythonAsync(pyscript);
console.log('done setting up environment');
return pyodide;
};
const loadPackage = async function (package_name: string[] | string, runtime: any): Promise<any> {
if (package_name.length > 0){
const micropip = pyodide.globals.get('micropip');
await micropip.install(package_name);
micropip.destroy();
}
};
const loadFromFile = async function (s: string, runtime: any): Promise<any> {
const filename = getLastPath(s);
await runtime.runPythonAsync(
`
from pyodide.http import pyfetch
from js import console
try:
response = await pyfetch("` +
s +
`")
except Exception as err:
console.warn("PyScript: Access to local files (using 'Paths:' in py-env) is not available when directly opening a HTML file; you must use a webserver to serve the additional files. See https://github.com/pyscript/pyscript/issues/257#issuecomment-1119595062 on starting a simple webserver with Python.")
raise(err)
content = await response.bytes()
with open("` +
filename +
`", "wb") as f:
f.write(content)
`,
);
};
export { loadInterpreter, pyodideReadyPromise, loadPackage, loadFromFile };

64
pyscriptjs/src/logger.ts Normal file
View File

@@ -0,0 +1,64 @@
/* Very simple logger interface.
Each module is expected to create its own logger by doing e.g.:
const logger = getLogger('my-prefix');
and then use it instead of console:
logger.info('hello', 'world');
logger.warn('...');
logger.error('...');
The logger automatically adds the prefix "[my-prefix]" to all logs; so e.g., the
above call would print:
[my-prefix] hello world
logger.log is intentionally omitted. The idea is that PyScript should not
write anything to console.log, to leave it free for the user.
Currently, the logger does not to anything more than that. In the future,
we might want to add additional features such as the ability to
enable/disable logs on a global or per-module basis.
*/
interface Logger {
debug(message: string, ...args: any[]): void;
info(message: string, ...args: any[]): void;
warn(message: string, ...args: any[]): void;
error(message: string, ...args: any[]): void;
}
const _cache = new Map<string, Logger>();
function getLogger(prefix: string): Logger {
let logger = _cache.get(prefix);
if (logger === undefined) {
logger = _makeLogger(prefix);
_cache.set(prefix, logger);
}
return logger;
}
function _makeLogger(prefix: string): Logger {
prefix = "[" + prefix + "] ";
function make(level: string) {
const out_fn = console[level].bind(console);
function fn(fmt: string, ...args: any[]) {
out_fn(prefix + fmt, ...args);
}
return fn
}
// 'log' is intentionally omitted
const debug = make('debug');
const info = make('info');
const warn = make('warn');
const error = make('error');
return {debug, info, warn, error};
}
export { getLogger };

View File

@@ -1,29 +1,23 @@
import App from './App.svelte'; import './styles/pyscript_base.css';
import { PyScript } from './components/pyscript'; import { PyScript } from './components/pyscript';
import { PyRepl } from './components/pyrepl';
import { PyEnv } from './components/pyenv'; import { PyEnv } from './components/pyenv';
import { PyBox } from './components/pybox';
import { PyButton } from './components/pybutton';
import { PyTitle } from './components/pytitle';
import { PyInputBox } from './components/pyinputbox';
import { PyWidget } from './components/base';
import { PyLoader } from './components/pyloader'; import { PyLoader } from './components/pyloader';
import { globalLoader } from './stores';
import { PyConfig } from './components/pyconfig'; import { PyConfig } from './components/pyconfig';
import { getLogger } from './logger';
import { globalLoader } from './stores';
const logger = getLogger('pyscript/main');
/* eslint-disable @typescript-eslint/no-unused-vars */
const xPyScript = customElements.define('py-script', PyScript); const xPyScript = customElements.define('py-script', PyScript);
const xPyRepl = customElements.define('py-repl', PyRepl);
const xPyEnv = customElements.define('py-env', PyEnv);
const xPyBox = customElements.define('py-box', PyBox);
const xPyButton = customElements.define('py-button', PyButton);
const xPyTitle = customElements.define('py-title', PyTitle);
const xPyInputBox = customElements.define('py-inputbox', PyInputBox);
const xPyWidget = customElements.define('py-register-widget', PyWidget);
const xPyLoader = customElements.define('py-loader', PyLoader); const xPyLoader = customElements.define('py-loader', PyLoader);
const xPyConfig = customElements.define('py-config', PyConfig); const xPyConfig = customElements.define('py-config', PyConfig);
const xPyEnv = customElements.define('py-env', PyEnv);
/* eslint-disable @typescript-eslint/no-unused-vars */
// As first thing, loop for application configs // As first thing, loop for application configs
logger.info('checking for py-config');
const config: PyConfig = document.querySelector('py-config'); const config: PyConfig = document.querySelector('py-config');
if (!config) { if (!config) {
const loader = document.createElement('py-config'); const loader = document.createElement('py-config');
@@ -31,12 +25,7 @@ if (!config) {
} }
// add loader to the page body // add loader to the page body
logger.info('add py-loader');
const loader = <PyLoader>document.createElement('py-loader'); const loader = <PyLoader>document.createElement('py-loader');
document.body.append(loader); document.body.append(loader);
globalLoader.set(loader); globalLoader.set(loader);
const app = new App({
target: document.body,
});
export default app;

109
pyscriptjs/src/pyodide.ts Normal file
View File

@@ -0,0 +1,109 @@
import { Runtime, RuntimeConfig } from './runtime';
import { getLastPath } from './utils';
import { getLogger } from './logger';
import type { PyodideInterface } from 'pyodide';
// eslint-disable-next-line
// @ts-ignore
import pyscript from './python/pyscript.py';
const logger = getLogger('pyscript/pyodide');
export class PyodideRuntime extends Runtime {
src: string;
name?: string;
lang?: string;
interpreter: PyodideInterface;
globals: any;
constructor(
src = 'https://cdn.jsdelivr.net/pyodide/v0.21.2/full/pyodide.js',
name = 'pyodide-default',
lang = 'python',
) {
logger.info('Runtime config:', { name, lang, src });
super();
this.src = src;
this.name = name;
this.lang = lang;
}
/**
* Although `loadPyodide` is used below,
* notice that it is not imported i.e.
* import { loadPyodide } from 'pyodide';
* is not used at the top of this file.
*
* This is because, if it's used, loadPyodide
* behaves mischievously i.e. it tries to load
* `pyodide.asm.js` and `pyodide_py.tar` but
* with paths that are wrong such as:
*
* http://127.0.0.1:8080/build/pyodide_py.tar
* which results in a 404 since `build` doesn't
* contain these files and is clearly the wrong
* path.
*/
async loadInterpreter(): Promise<void> {
logger.info('Loading pyodide');
// eslint-disable-next-line
// @ts-ignore
this.interpreter = await loadPyodide({
stdout: console.log,
stderr: console.log,
fullStdLib: false,
});
this.globals = this.interpreter.globals;
// XXX: ideally, we should load micropip only if we actually need it
await this.loadPackage('micropip');
logger.info('importing pyscript.py');
await this.run(pyscript);
logger.info('pyodide loaded and initialized');
}
async run(code: string): Promise<any> {
return await this.interpreter.runPythonAsync(code);
}
registerJsModule(name: string, module: object): void {
this.interpreter.registerJsModule(name, module);
}
async loadPackage(names: string | string[]): Promise<void> {
logger.info(`pyodide.loadPackage: ${names.toString()}`);
await this.interpreter.loadPackage(names,
logger.info.bind(logger),
logger.info.bind(logger));
}
async installPackage(package_name: string | string[]): Promise<void> {
if (package_name.length > 0) {
logger.info(`micropip install ${package_name.toString()}`);
const micropip = this.globals.get('micropip');
await micropip.install(package_name);
micropip.destroy();
}
}
async loadFromFile(path: string): Promise<void> {
const filename = getLastPath(path);
await this.run(
`
from pyodide.http import pyfetch
from js import console
try:
response = await pyfetch("${path}")
except Exception as err:
console.warn("PyScript: Access to local files (using 'paths:' in py-env) is not available when directly opening a HTML file; you must use a webserver to serve the additional files. See https://github.com/pyscript/pyscript/issues/257#issuecomment-1119595062 on starting a simple webserver with Python.")
raise(err)
content = await response.bytes()
with open("${filename}", "wb") as f:
f.write(content)
`,
);
}
}

View File

@@ -92,7 +92,7 @@ def format_mime(obj):
break break
if output is None: if output is None:
if not_available: if not_available:
console.warning( console.warn(
f"Rendered object requested unavailable MIME renderers: {not_available}" f"Rendered object requested unavailable MIME renderers: {not_available}"
) )
output = repr(output) output = repr(output)
@@ -149,8 +149,6 @@ class Element:
return self.element.innerHtml return self.element.innerHtml
def write(self, value, append=False): def write(self, value, append=False):
console.log(f"Element.write: {value} --> {append}")
out_element_id = self.id out_element_id = self.id
html, mime_type = format_mime(value) html, mime_type = format_mime(value)
@@ -186,7 +184,7 @@ class Element:
if _el: if _el:
return Element(_el.id, _el) return Element(_el.id, _el)
else: else:
console.log(f"WARNING: can't find element matching query {query}") console.warn(f"WARNING: can't find element matching query {query}")
def clone(self, new_id=None, to=None): def clone(self, new_id=None, to=None):
if new_id is None: if new_id is None:
@@ -261,21 +259,15 @@ class PyItemTemplate(Element):
self._id = None self._id = None
def create(self): def create(self):
console.log("creating section") new_child = create("div", self._id, "py-li-element")
new_child = create("section", self._id, "task bg-white my-1")
console.log("creating values")
console.log("creating innerHtml")
new_child._element.innerHTML = dedent( new_child._element.innerHTML = dedent(
f""" f"""
<label for="flex items-center p-2 "> <label id="{self._id}" for="flex items-center p-2 ">
<input class="mr-2" type="checkbox" class="task-check"> <input class="mr-2" type="checkbox" class="task-check">
<p class="m-0 inline">{self.render_content()}</p> <p>{self.render_content()}</p>
</label> </label>
""" """
) )
console.log("returning")
return new_child return new_child
def on_click(self, evt): def on_click(self, evt):
@@ -304,7 +296,7 @@ class PyItemTemplate(Element):
class PyListTemplate: class PyListTemplate:
theme = PyWidgetTheme("flex flex-col-reverse mt-8 mx-8") theme = PyWidgetTheme("py-li-element")
item_class = PyItemTemplate item_class = PyItemTemplate
def __init__(self, parent): def __init__(self, parent):
@@ -331,7 +323,6 @@ class PyListTemplate:
print(txt) print(txt)
def foo(evt): def foo(evt):
console.log(evt)
evtEl = evt.srcElement evtEl = evt.srcElement
srcEl = Element(binds[evtEl.id]) srcEl = Element(binds[evtEl.id])
srcEl.element.onclick() srcEl.element.onclick()
@@ -358,7 +349,6 @@ class PyListTemplate:
return self._add(child) return self._add(child)
def _add(self, child_elem): def _add(self, child_elem):
console.log("appending child", child_elem.element)
self.pre_child_append(child_elem) self.pre_child_append(child_elem)
child_elem.pre_append() child_elem.pre_append()
self._children.append(child_elem) self._children.append(child_elem)
@@ -387,19 +377,16 @@ class OutputCtxManager:
self._out = out self._out = out
self.output_to_console = output_to_console self.output_to_console = output_to_console
self._append = append self._append = append
console.log("----> changed out to", self._out, self._append)
def revert(self): def revert(self):
console.log("----> reverted")
self._out = self._prev self._out = self._prev
def write(self, value): def write(self, value):
console.log("writing to", self._out, value, self._append)
if self._out: if self._out:
Element(self._out).write(value, self._append) Element(self._out).write(value, self._append)
if self.output_to_console: if self.output_to_console:
console.log(self._out, value) console.info(value)
class OutputManager: class OutputManager:
@@ -430,7 +417,6 @@ class OutputManager:
self._err_manager.revert() self._err_manager.revert()
sys.stdout = self._out_manager sys.stdout = self._out_manager
sys.stderr = self._err_manager sys.stderr = self._err_manager
console.log("----> reverted")
pyscript = PyScript() pyscript = PyScript()

202
pyscriptjs/src/runtime.ts Normal file
View File

@@ -0,0 +1,202 @@
import type { PyodideInterface } from 'pyodide';
import type { PyLoader } from './components/pyloader';
import {
runtimeLoaded,
loadedEnvironments,
globalLoader,
initializers,
postInitializers,
Initializer,
scriptsQueue,
appConfig,
} from './stores';
import { createCustomElements } from './components/elements';
import type { PyScript } from './components/pyscript';
import { getLogger } from './logger';
const logger = getLogger('pyscript/runtime');
export const version = "<<VERSION>>";
export type RuntimeInterpreter = PyodideInterface | null;
export interface AppConfig extends Record<string, any> {
name?: string;
description?: string;
version?: string;
schema_version?: number;
type?: string;
author_name?: string;
author_email?: string;
license?: string;
autoclose_loader?: boolean;
runtimes?: Array<RuntimeConfig>;
packages?: Array<string>;
paths?: Array<string>;
plugins?: Array<string>;
pyscript?: PyScriptMetadata;
}
export type PyScriptMetadata = {
version?: string;
time?: string;
}
export type RuntimeConfig = {
src?: string;
name?: string;
lang?: string;
};
let loader: PyLoader | undefined;
globalLoader.subscribe(value => {
loader = value;
});
let initializers_: Initializer[];
initializers.subscribe((value: Initializer[]) => {
initializers_ = value;
});
let postInitializers_: Initializer[];
postInitializers.subscribe((value: Initializer[]) => {
postInitializers_ = value;
});
let scriptsQueue_: PyScript[];
scriptsQueue.subscribe((value: PyScript[]) => {
scriptsQueue_ = value;
});
let appConfig_: AppConfig = {
autoclose_loader: true
};
appConfig.subscribe((value: AppConfig) => {
if (value) {
appConfig_ = value;
}
});
/*
Runtime class is a super class that all different runtimes must respect
and adhere to.
Currently, the only runtime available is Pyodide as indicated by the
`RuntimeInterpreter` type above. This serves as a Union of types of
different runtimes/interpreters which will be added in near future.
The class has abstract methods available which each runtime is supposed
to implement.
Methods available handle loading of the interpreter, initialization,
running code, loading and installation of packages, loading from files etc.
For an example implementation, refer to the `PyodideRuntime` class
in `pyodide.ts`
*/
export abstract class Runtime extends Object {
abstract src: string;
abstract name?: string;
abstract lang?: string;
abstract interpreter: RuntimeInterpreter;
/**
* global symbols table for the underlying interpreter.
* */
abstract globals: any;
/**
* loads the interpreter for the runtime and saves an instance of it
* in the `this.interpreter` property along with calling of other
* additional convenience functions.
* */
abstract loadInterpreter(): Promise<void>;
/**
* delegates the code to be run to the underlying interpreter
* (asynchronously) which can call its own API behind the scenes.
* */
abstract run(code: string): Promise<any>;
/**
* delegates the setting of JS objects to
* the underlying interpreter.
* */
abstract registerJsModule(name: string, module: object): void;
/**
* delegates the loading of packages to
* the underlying interpreter.
* */
abstract loadPackage(names: string | string[]): Promise<void>;
/**
* delegates the installation of packages
* (using a package manager, which can be specific to
* the runtime) to the underlying interpreter.
*
* For Pyodide, we use `micropip`
* */
abstract installPackage(package_name: string | string[]): Promise<void>;
/**
* delegates the loading of files to the
* underlying interpreter.
* */
abstract loadFromFile(path: string): Promise<void>;
/**
* initializes the page which involves loading of runtime,
* as well as evaluating all the code inside <py-script> tags
* along with initializers and postInitializers
* */
async initialize(): Promise<void> {
loader?.log('Loading runtime...');
await this.loadInterpreter();
const newEnv = {
id: 'default',
runtime: this,
state: 'loading',
};
runtimeLoaded.set(this);
// Inject the loader into the runtime namespace
// eslint-disable-next-line
this.globals.set('pyscript_loader', loader);
loader?.log('Runtime created...');
loadedEnvironments.update(environments => ({
...environments,
[newEnv['id']]: newEnv,
}));
// now we call all initializers before we actually executed all page scripts
loader?.log('Initializing components...');
for (const initializer of initializers_) {
await initializer();
}
loader?.log('Initializing scripts...');
for (const script of scriptsQueue_) {
void script.evaluate();
}
scriptsQueue.set([]);
// now we call all post initializers AFTER we actually executed all page scripts
loader?.log('Running post initializers...');
// Finally create the custom elements for pyscript such as pybutton
createCustomElements();
if (appConfig_ && appConfig_.autoclose_loader) {
loader?.close();
}
for (const initializer of postInitializers_) {
await initializer();
}
// NOTE: this message is used by integration tests to know that
// pyscript initialization has complete. If you change it, you need to
// change it also in tests/integration/support.py
logger.info('PyScript page fully initialized');
}
}

View File

@@ -1,57 +1,44 @@
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
import type { PyLoader } from './components/pyloader'; import type { PyLoader } from './components/pyloader';
import type { PyScript } from './components/pyscript'; import type { PyScript } from './components/pyscript';
import type { Runtime, AppConfig } from './runtime';
import { getLogger } from './logger';
export type Initializer = () => Promise<void>; export type Initializer = () => Promise<void>;
export const pyodideLoaded = writable({ export type Environment = {
loaded: false, id: string;
premise: null, runtime: Runtime;
}); state: string;
};
export const loadedEnvironments = writable([{}]); /*
export const DEFAULT_MODE = 'play'; A store for Runtime which can encompass any
runtime, but currently only has Pyodide as its offering.
*/
export const runtimeLoaded = writable<Runtime>();
export const loadedEnvironments = writable<Record<Environment['id'], Environment>>({});
export const navBarOpen = writable(false); export const navBarOpen = writable(false);
export const componentsNavOpen = writable(false); export const componentsNavOpen = writable(false);
export const componentDetailsNavOpen = writable(false); export const componentDetailsNavOpen = writable(false);
export const mainDiv = writable(null); export const mainDiv = writable(null);
export const currentComponentDetails = writable([]); export const currentComponentDetails = writable([]);
export const mode = writable(DEFAULT_MODE);
export const scriptsQueue = writable<PyScript[]>([]); export const scriptsQueue = writable<PyScript[]>([]);
export const initializers = writable<Initializer[]>([]); export const initializers = writable<Initializer[]>([]);
export const postInitializers = writable<Initializer[]>([]); export const postInitializers = writable<Initializer[]>([]);
export const globalLoader = writable<PyLoader | undefined>(); export const globalLoader = writable<PyLoader | undefined>();
export const appConfig = writable(); export const appConfig = writable<AppConfig>();
let scriptsQueue_: PyScript[] = [];
let initializers_: Initializer[] = [];
let postInitializers_: Initializer[] = [];
scriptsQueue.subscribe(value => {
scriptsQueue_ = value;
});
export const addToScriptsQueue = (script: PyScript) => { export const addToScriptsQueue = (script: PyScript) => {
scriptsQueue.set([...scriptsQueue_, script]); scriptsQueue.update(scriptsQueue => [...scriptsQueue, script]);
}; };
initializers.subscribe(value => {
initializers_ = value;
});
export const addInitializer = (initializer: Initializer) => { export const addInitializer = (initializer: Initializer) => {
console.log('adding initializer', initializer); initializers.update(initializers => [...initializers, initializer]);
initializers.set([...initializers_, initializer]);
console.log('added initializer', initializer);
}; };
postInitializers.subscribe(value => {
postInitializers_ = value;
});
export const addPostInitializer = (initializer: Initializer) => { export const addPostInitializer = (initializer: Initializer) => {
console.log('adding post initializer', initializer); postInitializers.update(postInitializers => [...postInitializers, initializer]);
postInitializers.set([...postInitializers_, initializer]);
console.log('added post initializer', initializer);
}; };

View File

@@ -0,0 +1,244 @@
:not(:defined) {
display: none
}
html{
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
line-height: 1.5;
}
.spinner::after {
content: '';
box-sizing: border-box;
width: 40px;
height: 40px;
position: absolute;
top: calc(40% - 20px);
left: calc(50% - 20px);
border-radius: 50%;
}
.spinner.smooth::after {
border-top: 4px solid rgba(255, 255, 255, 1);
border-left: 4px solid rgba(255, 255, 255, 1);
border-right: 4px solid rgba(255, 255, 255, 0);
animation: spinner 0.6s linear infinite;
}
@keyframes spinner {
to {
transform: rotate(360deg);
}
}
.label {
text-align: center;
width: 100%;
display: block;
color: rgba(255, 255, 255, 0.8);
font-size: 0.8rem;
margin-top: 6rem;
}
/* Pop-up second layer begin */
.py-overlay {
position: fixed;
display: flex;
justify-content: center;
align-items: center;
color: white;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.5);
transition: opacity 500ms;
visibility: hidden;
color: visible;
opacity: 1;
}
.py-overlay {
visibility: visible;
opacity: 1;
}
.py-pop-up {
text-align: center;
width: 600px;
}
.py-pop-up p {
margin: 5px;
}
.py-pop-up a {
position: absolute;
color: white;
text-decoration: none;
font-size: 200%;
top: 3.5%;
right: 5%;
}
.py-box{
display: flex;
flex-direction: row;
justify-content: flex-start;
}
.py-box div.py-box-child *
{
max-width: 100%;
}
.py-repl-box{
flex-direction: column;
}
.editor-box{
--tw-border-opacity: 1;
border-color: rgba(209, 213, 219, var(--tw-border-opacity));
border-width: 1px;
position: relative;
--tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgba(59, 130, 246, 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
position: relative;
box-sizing: border-box;
border-width: 1px;
border-style: solid;
border-color: rgb(209, 213, 219)
}
.editor-box:hover button{
opacity: 1;
}
.repl-play-button{
opacity: 0;
bottom: 0.25rem;
right: 0.25rem;
position: absolute;
padding: 0;
line-height: inherit;
color: inherit;
cursor: pointer;
background-color: transparent;
background-image: none;
-webkit-appearance: button;
text-transform: none;
font-family: inherit;
font-size: 100%;
margin: 0;
text-rendering: auto;
letter-spacing: normal;
word-spacing: normal;
line-height: normal;
text-transform: none;
text-indent: 0px;
text-shadow: none;
display: inline-block;
text-align: center;
align-items: flex-start;
cursor: default;
box-sizing: border-box;
background-color: -internal-light-dark(rgb(239, 239, 239), rgb(59, 59, 59));
margin: 0em;
padding: 1px 6px;
border: 0;
}
.repl-play-button:hover{
opacity: 1;
}
.py-title{
text-transform: uppercase;
text-align: center;
}
.py-title h1{
font-weight: 700;
font-size: 1.875rem;
}
.py-input{
padding: 0.5rem;
--tw-border-opacity: 1;
border-color: rgba(209, 213, 219, var(--tw-border-opacity));
border-width: 1px;
border-radius: 0.25rem;
margin-right: 0.75rem;
border-style: solid;
width: auto;
}
.py-box input.py-input{
width: -webkit-fill-available;
}
.central-content{
max-width: 20rem;
margin-left: auto;
margin-right: auto;
}
input {
text-rendering: auto;
color: -internal-light-dark(black, white);
letter-spacing: normal;
word-spacing: normal;
line-height: normal;
text-transform: none;
text-indent: 0px;
text-shadow: none;
display: inline-block;
text-align: start;
appearance: auto;
-webkit-rtl-ordering: logical;
cursor: text;
background-color: -internal-light-dark(rgb(255, 255, 255), rgb(59, 59, 59));
margin: 0em;
padding: 1px 2px;
border-width: 2px;
border-style: inset;
border-color: -internal-light-dark(rgb(118, 118, 118), rgb(133, 133, 133));
border-image: initial;
}
.py-button{
--tw-text-opacity: 1;
color: rgba(255, 255, 255, var(--tw-text-opacity));
padding: 0.5rem;
--tw-bg-opacity: 1;
background-color: rgba(37, 99, 235, var(--tw-bg-opacity));
--tw-border-opacity: 1;
border-color: rgba(37, 99, 235, var(--tw-border-opacity));
border-width: 1px;
border-radius: 0.25rem;
}
.py-li-element p{
margin: 5px;
}
.py-li-element p{
display: inline;
}
button, input, optgroup, select, textarea {
font-family: inherit;
font-size: 100%;
line-height: 1.15;
margin: 0;
}
.line-through {
text-decoration: line-through;
}

360
pyscriptjs/src/toml.ts Normal file
View File

@@ -0,0 +1,360 @@
// taken from https://github.com/Gin-Quin/fast-toml
/* eslint-disable */
"use strict";
let e = "",
t = 0;
function i(e:any, t:any = 0):any {
let i;
for (; (i = e[t++]) && (" " == i || "\t" == i || "\r" == i); );
return t - 1;
}
function n(e:any):any {
switch (e[0]) {
case void 0:
return "";
case '"':
return (function (e) {
let t,
i = 0,
n = "";
for (; (t = e.indexOf("\\", i) + 1); ) {
switch (((n += e.slice(i, t - 1)), e[t])) {
case "\\":
n += "\\";
break;
case '"':
n += '"';
break;
case "\r":
"\n" == e[t + 1] && t++;
case "\n":
break;
case "b":
n += "\b";
break;
case "t":
n += "\t";
break;
case "n":
n += "\n";
break;
case "f":
n += "\f";
break;
case "r":
n += "\r";
break;
case "u":
(n += String.fromCharCode(parseInt(e.substr(t + 1, 4), 16))), (t += 4);
break;
case "U":
(n += String.fromCharCode(parseInt(e.substr(t + 1, 8), 16))), (t += 8);
break;
default:
throw r(e[t]);
}
i = t + 1;
}
return n + e.slice(i);
})(e.slice(1, -1));
case "'":
return e.slice(1, -1);
case "0":
case "1":
case "2":
case "3":
case "4":
case "5":
case "6":
case "7":
case "8":
case "9":
case "+":
case "-":
case ".":
let t = e;
if ((-1 != t.indexOf("_") && (t = t.replace(/_/g, "")), !isNaN(t))) return +t;
if ("-" == e[4] && "-" == e[7]) {
let t = new Date(e);
if ("Invalid Date" != t.toString()) return t;
} else if (":" == e[2] && ":" == e[5] && e.length >= 7) {
let t = new Date("0000-01-01T" + e + "Z");
if ("Invalid Date" != t.toString()) return t;
}
return e;
}
switch (e) {
case "true":
return !0;
case "false":
return !1;
case "nan":
case "NaN":
return !1;
case "null":
return null;
case "inf":
case "+inf":
case "Infinity":
case "+Infinity":
return 1 / 0;
case "-inf":
case "-Infinity":
return -1 / 0;
}
return e;
}
function r(i:any):any {
let n = (function () {
let i = e[t],
n = t;
"\n" == i && n--;
let r = 1,
s = e.lastIndexOf("\n", n),
a = e.indexOf("\n", n);
-1 == a && (a = 1 / 0);
("," != i && "\n" != i) || (n = s + 1);
if (-1 == s) return { line: r, column: n + 1, position: n, lineContent: e.slice(0, a).trim() };
const c = n - s + 1,
o = e.slice(s + 1, a).trim();
r++;
for (; -1 != (s = e.lastIndexOf("\n", s - 1)); ) r++;
return { line: r, column: c, position: n, lineContent: o };
})(),
r = String(n.line);
return (i += "\n" + r + " | " + n.lineContent + "\n"), (i += " ".repeat(r.length + n.column + 2) + "^"), SyntaxError(i);
}
function s(e:any, i:any = 0, n:any = !1):any {
let a,
c = e[i],
o = c,
f = c,
l = !0,
u = !1;
switch (c) {
case '"':
case "'":
if (((a = i + 1), n && e[i + 1] == c && e[i + 2] == c ? ((f = c + c + c), (a += 2)) : (u = !0), "'" == c)) a = e.indexOf(f, a) + 1;
else
for (; (a = e.indexOf(f, a) + 1); ) {
let t = !0,
i = a - 1;
for (; "\\" == e[--i]; ) t = !t;
if (t) break;
}
if (!a) throw r("Missing " + f + " closer");
if (c != f) a += 2;
else if (u) {
let n = e.indexOf("\n", i + 1) + 1;
if (n && n < a) throw ((t = n - 2), r("Forbidden end-of-line character in single-line string"));
}
return a;
case "(":
f = ")";
break;
case "{":
f = "}";
break;
case "[":
f = "]";
break;
case "<":
f = ">";
break;
default:
l = !1;
}
let h = 0;
for (; (c = e[++i]); )
if (c == f) {
if (0 == h) return i + 1;
h--;
} else if ('"' == c || "'" == c) {
i = s(e, i, n) - 1;
} else l && c == o && h++;
throw r("Missing " + f);
}
function a(e:any):any {
"string" != typeof e && (e = String(e));
let t:any,
i:any,
n:any = -1,
a:any = "",
c:any = [];
for (; (i = e[++n]); )
switch (i) {
case ".":
if (!a) throw r('Unexpected "."');
c.push(a), (a = "");
continue;
case '"':
case "'":
if (((t = s(e, n)), t == n + 2)) throw r("Empty string key");
(a += e.slice(n + 1, t - 1)), (n = t - 1);
continue;
default:
a += i;
}
return a && c.push(a), c;
}
function c(e:any, t:any = []):any {
const i = t.pop();
for (let i of t) {
if ("object" != typeof e) {
throw r('["' + t.slice(0, t.indexOf(i) + 1).join('"].["') + '"]' + " must be an object");
}
void 0 === e[i] && (e[i] = {}), (e = e[i]) instanceof Array && (e = e[e.length - 1]);
}
return [e, i];
}
class o {
root:any;
data:any;
inlineScopeList: any[];
constructor() {
this.root = {};
this.data = this.root;
this.inlineScopeList = [];
}
get isRoot():any {
return this.data == this.root;
}
set(e:any, t:any):any {
let [i, n] = c(this.data, a(e));
if ("string" == typeof i) throw "Wtf the scope is a string. Please report the bug";
if (n in i) throw r(`Re-writing the key '${e}'`);
return (i[n] = t), t;
}
push(e:any):any {
if (!(this.data instanceof Array)) {
if (!this.isRoot) throw r("Missing key");
(this.data = Object.assign([], this.data)), (this.root = this.data);
}
return this.data.push(e), this;
}
use(e:any):any {
return (
(this.data = (function (e:any, t:any = []) {
for (let i of t) {
//if (void 0 === e) e = lastData[lastElt] = {};
//else
if ("object" != typeof e) {
throw r('["' + t.slice(0, t.indexOf(i) + 1).join('"].["') + '"]' + " must be an object");
}
void 0 === e[i] && (e[i] = {}), (e = e[i]) instanceof Array && (e = e[e.length - 1]);
}
return e;
})(this.root, a(e))),
this
);
}
useArray(e:any):any {
let [t, i] = c(this.root, a(e));
return (this.data = {}), void 0 === t[i] && (t[i] = []), t[i].push(this.data), this;
}
enter(e:any, t:any):any {
return this.inlineScopeList.push(this.data), this.set(e, t), (this.data = t), this;
}
enterArray(e:any):any {
return this.inlineScopeList.push(this.data), this.push(e), (this.data = e), this;
}
exit():any {
return (this.data = this.inlineScopeList.pop()), this;
}
}
function f(a:any):any {
"string" != typeof a && (a = String(a));
const c = new o(),
f:any[] = [];
(e = a), (t = 0);
let l:any,
u:any,
h:any = "",
d:any = "",
p:any = e[0],
w:any = !0;
const g = ():any => {
if (((h = h.trimEnd()), w)) h && c.push(n(h));
else {
if (!h) throw r("Expected key before =");
if (!d) throw r("Expected value after =");
c.set(h, n(d.trimEnd()));
}
(h = ""), (d = ""), (w = !0);
};
do {
switch (p) {
case " ":
w ? h && (h += p) : d && (d += p);
case "\t":
case "\r":
continue;
case "#":
(t = e.indexOf("\n", t + 1) - 1), -2 == t && (t = 1 / 0);
continue;
case '"':
case "'":
if (!w && d) {
d += p;
continue;
}
let n = e[t + 1] == p && e[t + 2] == p;
if (((l = s(e, t, !0)), w)) {
if (h) throw r("Unexpected " + p);
(h += n ? e.slice(t + 2, l - 2) : e.slice(t, l)), (t = l);
} else (d = e.slice(t, l)), (t = l), n && ((d = d.slice(2, -2)), "\n" == d[1] ? (d = d[0] + d.slice(2)) : "\r" == d[1] && "\n" == d[2] && (d = d[0] + d.slice(3)));
if (((t = i(e, t)), (p = e[t]), p && "," != p && "\n" != p && "#" != p && "}" != p && "]" != p && "=" != p)) throw r("Unexpected character after end of string");
t--;
continue;
case "\n":
case ",":
case void 0:
g();
continue;
case "[":
case "{":
if (((u = "[" == p ? "]" : "}"), w && !f.length)) {
if (h) throw r("Unexpected " + p);
if (((l = s(e, t)), "[" == p && "[" == e[t + 1])) {
if ("]" != e[l - 2]) throw r("Missing ]]");
c.useArray(e.slice(t + 2, l - 2));
} else c.use(e.slice(t + 1, l - 1));
t = l;
} else if (w) {
if (h) throw r("Unexpected " + p);
c.enterArray("[" == p ? [] : {}), f.push(u);
} else {
if (d) throw r("Unexpected " + p);
c.enter(h.trimEnd(), "[" == p ? [] : {}), f.push(u), (h = ""), (w = !0);
}
continue;
case "]":
case "}":
if ((h && g(), f.pop() != p)) throw r("Unexpected " + p);
if ((c.exit(), (t = i(e, t + 1)), (p = e[t]), p && "," != p && "\n" != p && "#" != p && "}" != p && "]" != p)) throw r("Unexpected character after end of scope");
t--;
continue;
case "=":
if (!w) throw r("Unexpected " + p);
if (!h) throw r("Missing key before " + p);
w = !1;
continue;
default:
w ? (h += p) : (d += p);
}
} while ((p = e[++t]) || h);
if (f.length) throw r("Missing " + f.pop());
return c.root;
}
let l = null,
u = null;
function h():any {
let e = "";
for (let t of arguments) e += "string" == typeof t ? t : t[0];
return f(e);
}
export const toml = (
(h.parse = f),
h
);

View File

@@ -1,3 +1,27 @@
import {toml} from './toml'
import type { AppConfig } from "./runtime";
const allKeys = {
"string": ["name", "description", "version", "type", "author_name", "author_email", "license"],
"number": ["schema_version"],
"boolean": ["autoclose_loader"],
"array": ["runtimes", "packages", "paths", "plugins"]
};
const defaultConfig: AppConfig = {
"schema_version": 1,
"type": "app",
"autoclose_loader": true,
"runtimes": [{
"src": "https://cdn.jsdelivr.net/pyodide/v0.21.2/full/pyodide.js",
"name": "pyodide-0.21.2",
"lang": "python"
}],
"packages":[],
"paths":[],
"plugins": []
}
function addClasses(element: HTMLElement, classes: Array<string>) { function addClasses(element: HTMLElement, classes: Array<string>) {
for (const entry of classes) { for (const entry of classes) {
element.classList.add(entry); element.classList.add(entry);
@@ -14,8 +38,12 @@ function getLastPath(str: string): string {
return str.split('\\').pop().split('/').pop(); return str.split('\\').pop().split('/').pop();
} }
function escape(str: string): string {
return str.replace(/</g, "&lt;").replace(/>/g, "&gt;")
}
function htmlDecode(input: string): string { function htmlDecode(input: string): string {
const doc = new DOMParser().parseFromString(ltrim(input), 'text/html'); const doc = new DOMParser().parseFromString(ltrim(escape(input)), 'text/html');
return doc.documentElement.textContent; return doc.documentElement.textContent;
} }
@@ -60,7 +88,7 @@ function showError(msg: string): void {
function handleFetchError(e: Error, singleFile: string) { function handleFetchError(e: Error, singleFile: string) {
//Should we still export full error contents to console? //Should we still export full error contents to console?
console.warn('Caught an error in loadPaths:\r\n' + e); console.warn(`Caught an error in loadPaths:\r\n ${e.toString()}`);
let errorContent: string; let errorContent: string;
if (e.message.includes('TypeError: Failed to fetch')) { if (e.message.includes('TypeError: Failed to fetch')) {
errorContent = `<p>PyScript: Access to local files errorContent = `<p>PyScript: Access to local files
@@ -80,4 +108,152 @@ function handleFetchError(e: Error, singleFile: string) {
showError(errorContent); showError(errorContent);
} }
export { addClasses, removeClasses, getLastPath, ltrim, htmlDecode, guidGenerator, showError, handleFetchError }; function readTextFromPath(path: string) {
const request = new XMLHttpRequest();
request.open("GET", path, false);
request.send();
const returnValue = request.responseText;
return returnValue;
}
function inJest(): boolean {
return typeof process === 'object' && process.env.JEST_WORKER_ID !== undefined;
}
function fillUserData(inputConfig: AppConfig, resultConfig: AppConfig): AppConfig
{
for (const key in inputConfig)
{
// fill in all extra keys ignored by the validator
if (!(key in defaultConfig))
{
resultConfig[key] = inputConfig[key];
}
}
return resultConfig;
}
function mergeConfig(inlineConfig: AppConfig, externalConfig: AppConfig): AppConfig {
if (Object.keys(inlineConfig).length === 0 && Object.keys(externalConfig).length === 0)
{
return defaultConfig;
}
else if (Object.keys(inlineConfig).length === 0)
{
return externalConfig;
}
else if(Object.keys(externalConfig).length === 0)
{
return inlineConfig;
}
else
{
let merged: AppConfig = {};
for (const keyType in allKeys)
{
const keys = allKeys[keyType];
keys.forEach(function(item: string){
if (keyType === "boolean")
{
merged[item] = (typeof inlineConfig[item] !== "undefined") ? inlineConfig[item] : externalConfig[item];
}
else
{
merged[item] = inlineConfig[item] || externalConfig[item];
}
});
}
// fill extra keys from external first
// they will be overridden by inline if extra keys also clash
merged = fillUserData(externalConfig, merged);
merged = fillUserData(inlineConfig, merged);
return merged;
}
}
function parseConfig(configText: string, configType = "toml") {
let config: object;
if (configType === "toml") {
try {
// TOML parser is soft and can parse even JSON strings, this additional check prevents it.
if (configText.trim()[0] === "{")
{
const errMessage = `config supplied: ${configText} is an invalid TOML and cannot be parsed`;
showError(`<p>${errMessage}</p>`);
throw Error(errMessage);
}
config = toml.parse(configText);
}
catch (err) {
const errMessage: string = err.toString();
showError(`<p>config supplied: ${configText} is an invalid TOML and cannot be parsed: ${errMessage}</p>`);
throw err;
}
}
else if (configType === "json") {
try {
config = JSON.parse(configText);
}
catch (err) {
const errMessage: string = err.toString();
showError(`<p>config supplied: ${configText} is an invalid JSON and cannot be parsed: ${errMessage}</p>`);
throw err;
}
}
else {
showError(`<p>type of config supplied is: ${configType}, supported values are ["toml", "json"].</p>`);
}
return config;
}
function validateConfig(configText: string, configType = "toml") {
const config = parseConfig(configText, configType);
const finalConfig: AppConfig = {}
for (const keyType in allKeys)
{
const keys = allKeys[keyType];
keys.forEach(function(item: string){
if (validateParamInConfig(item, keyType, config))
{
if (item === "runtimes")
{
finalConfig[item] = [];
const runtimes = config[item];
runtimes.forEach(function(eachRuntime: object){
const runtimeConfig: object = {};
for (const eachRuntimeParam in eachRuntime)
{
if (validateParamInConfig(eachRuntimeParam, "string", eachRuntime))
{
runtimeConfig[eachRuntimeParam] = eachRuntime[eachRuntimeParam];
}
}
finalConfig[item].push(runtimeConfig);
});
}
else
{
finalConfig[item] = config[item];
}
}
});
}
return fillUserData(config, finalConfig);
}
function validateParamInConfig(paramName: string, paramType: string, config: object): boolean {
if (paramName in config)
{
return paramType === "array" ? Array.isArray(config[paramName]) : typeof config[paramName] === paramType;
}
return false;
}
export { defaultConfig, addClasses, removeClasses, getLastPath, ltrim, htmlDecode, guidGenerator, showError, handleFetchError, readTextFromPath, inJest, mergeConfig, validateConfig };

View File

@@ -1,29 +0,0 @@
const { tailwindExtractor } = require("tailwindcss/lib/lib/purgeUnusedStyles");
module.exports = {
purge: {
content: ["src/**/*.svelte", "public/index.html"],
options: {
defaultExtractor: (content) => [
...tailwindExtractor(content),
...[...content.matchAll(/(?:class:)*([\w\d-/:%.]+)/gm)].map(
([_match, group, ..._rest]) => group
),
],
keyframes: true,
},
},
darkMode: false, // or 'media' or 'class'
theme: {
extend: {},
container: {
center: true
}
},
variants: {
extend: {
display: ['group-hover']
},
},
plugins: [],
};

View File

@@ -1,14 +1,16 @@
"""All data required for testing examples""" """All data required for testing examples"""
import os
import threading import threading
from http.server import HTTPServer as SuperHTTPServer from http.server import HTTPServer as SuperHTTPServer
from http.server import SimpleHTTPRequestHandler from http.server import SimpleHTTPRequestHandler
from pathlib import Path
import pytest import pytest
my_path = Path.cwd() / "examples" from .support import Logger
os.chdir(my_path)
@pytest.fixture(scope="session")
def logger():
return Logger()
class HTTPServer(SuperHTTPServer): class HTTPServer(SuperHTTPServer):
@@ -28,12 +30,17 @@ class HTTPServer(SuperHTTPServer):
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def http_server(): def http_server(logger):
class MyHTTPRequestHandler(SimpleHTTPRequestHandler):
def log_message(self, fmt, *args):
logger.log("http_server", fmt % args, color="blue")
host, port = "127.0.0.1", 8080 host, port = "127.0.0.1", 8080
base_url = f"http://{host}:{port}" base_url = f"http://{host}:{port}"
# serve_Run forever under thread # serve_Run forever under thread
server = HTTPServer((host, port), SimpleHTTPRequestHandler) server = HTTPServer((host, port), MyHTTPRequestHandler)
thread = threading.Thread(None, server.run) thread = threading.Thread(None, server.run)
thread.start() thread.start()

View File

@@ -0,0 +1,412 @@
import pdb
import re
import time
import py
import pytest
ROOT = py.path.local(__file__).dirpath("..", "..", "..")
BUILD = ROOT.join("pyscriptjs", "build")
@pytest.mark.usefixtures("init")
class PyScriptTest:
"""
Base class to write PyScript integration tests, based on playwright.
It provides a simple API to generate HTML files and load them in
playwright.
It also provides a Pythonic API on top of playwright for the most
common tasks; in particular:
- self.console collects all the JS console.* messages. Look at the doc
of ConsoleMessageCollection for more details.
- self.check_errors() checks that no JS errors have been thrown
- after each test, self.check_errors() is automatically run to ensure
that no JS error passes uncaught.
- self.wait_for_console waits until the specified message appears in the
console
- self.wait_for_pyscript waits until all the PyScript tags have been
evaluated
- self.pyscript_run is the main entry point for pyscript tests: it
creates an HTML page to run the specified snippet.
"""
# Pyodide always print()s this message upon initialization. Make it
# available to all tests so that it's easiert to check.
PY_COMPLETE = "Python initialization complete"
@pytest.fixture()
def init(self, request, tmpdir, http_server, logger, page):
"""
Fixture to automatically initialize all the tests in this class and its
subclasses.
The magic is done by the decorator @pyest.mark.usefixtures("init"),
which tells pytest to automatically use this fixture for all the test
method of this class.
Using the standard pytest behavior, we can request more fixtures:
tmpdir, http_server and page; 'page' is a fixture provided by
pytest-playwright.
Then, we save these fixtures on the self and proceed with more
initialization. The end result is that the requested fixtures are
automatically made available as self.xxx in all methods.
"""
self.testname = request.function.__name__.replace("test_", "")
self.tmpdir = tmpdir
# create a symlink to BUILD inside tmpdir
tmpdir.join("build").mksymlinkto(BUILD)
self.tmpdir.chdir()
self.http_server = http_server
self.logger = logger
self.init_page(page)
#
# this extra print is useful when using pytest -s, else we start printing
# in the middle of the line
print()
#
# if you use pytest --headed you can see the browser page while
# playwright executes the tests, but the page is closed very quickly
# as soon as the test finishes. To avoid that, we automatically start
# a pdb so that we can wait as long as we want.
yield
if request.config.option.headed:
pdb.Pdb.intro = (
"\n"
"This (Pdb) was started automatically because you passed --headed:\n"
"the execution of the test pauses here to give you the time to inspect\n"
"the browser. When you are done, type one of the following commands:\n"
" (Pdb) continue\n"
" (Pdb) cont\n"
" (Pdb) c\n"
)
pdb.set_trace()
def init_page(self, page):
self.page = page
self.console = ConsoleMessageCollection(self.logger)
self._page_errors = []
page.on("console", self.console.add_message)
page.on("pageerror", self._on_pageerror)
def teardown_method(self):
# we call check_errors on teardown: this means that if there are still
# non-cleared errors, the test will fail. If you expect errors in your
# page and they should not cause the test to fail, you should call
# self.check_errors() in the test itself.
self.check_errors()
def _on_pageerror(self, error):
self.logger.log("JS exception", error.stack, color="red")
self._page_errors.append(error)
def check_errors(self):
"""
Check whether JS errors were reported.
If it finds a single JS error, raise JsError.
If it finds multiple JS errors, raise JsMultipleErrors.
Upon return, all the errors are cleared, so a subsequent call to
check_errors will not raise, unless NEW JS errors have been reported
in the meantime.
"""
exc = None
if len(self._page_errors) == 1:
# if there is a single error, wrap it
exc = JsError(self._page_errors[0])
elif len(self._page_errors) >= 2:
exc = JsMultipleErrors(self._page_errors)
self._page_errors = []
if exc:
raise exc
def clear_errors(self):
"""
Clear all JS errors.
"""
self._page_errors = []
def writefile(self, filename, content):
"""
Very thin helper to write a file in the tmpdir
"""
f = self.tmpdir.join(filename)
f.write(content)
def goto(self, path):
self.logger.reset()
self.logger.log("page.goto", path, color="yellow")
url = f"{self.http_server}/{path}"
self.page.goto(url)
def wait_for_console(self, text, *, timeout=None, check_errors=True):
"""
Wait until the given message appear in the console.
Note: it must be the *exact* string as printed by e.g. console.log.
If you need more control on the predicate (e.g. if you want to match a
substring), use self.page.expect_console_message directly.
timeout is expressed in milliseconds. If it's None, it will use
playwright's own default value, which is 30 seconds).
If check_errors is True (the default), it also checks that no JS
errors were raised during the waiting.
"""
pred = lambda msg: msg.text == text
try:
with self.page.expect_console_message(pred, timeout=timeout):
pass
finally:
# raise JsError if there were any javascript exception. Note that
# this might happen also in case of a TimeoutError. In that case,
# the JsError will shadow the TimeoutError but this is correct,
# because it's very likely that the console message never appeared
# precisely because of the exception in JS.
if check_errors:
self.check_errors()
def wait_for_pyscript(self, *, timeout=None, check_errors=True):
"""
Wait until pyscript has been fully loaded.
Timeout is expressed in milliseconds. If it's None, it will use
playwright's own default value, which is 30 seconds).
If check_errors is True (the default), it also checks that no JS
errors were raised during the waiting.
"""
# this is printed by runtime.ts:Runtime.initialize
self.wait_for_console(
"[pyscript/runtime] PyScript page fully initialized",
timeout=timeout,
check_errors=check_errors,
)
# We still don't know why this wait is necessary, but without it
# events aren't being triggered in the tests.
self.page.wait_for_timeout(100)
def pyscript_run(self, snippet, *, extra_head=""):
"""
Main entry point for pyscript tests.
snippet contains a fragment of HTML which will be put inside a full
HTML document. In particular, the <head> automatically contains the
correct <script> and <link> tags which are necessary to load pyscript
correctly.
This method does the following:
- write a full HTML file containing the snippet
- open a playwright page for it
- wait until pyscript has been fully loaded
"""
doc = f"""
<html>
<head>
<link rel="stylesheet" href="{self.http_server}/build/pyscript.css" />
<script defer src="{self.http_server}/build/pyscript.js"></script>
{extra_head}
</head>
<body>
{snippet}
</body>
</html>
"""
filename = f"{self.testname}.html"
self.writefile(filename, doc)
self.goto(filename)
self.wait_for_pyscript()
# ============== Helpers and utility functions ==============
class JsError(Exception):
"""
Represent an exception which happened in JS.
It's a thin wrapper around playwright.sync_api.Error, with two important
differences:
1. it has a better name: if you see JsError in a traceback, it's
immediately obvious that it's a JS exception.
2. Show also the JS stacktrace by default, contrarily to
playwright.sync_api.Error
"""
def __init__(self, error):
super().__init__(self.format_playwright_error(error))
self.error = error
@staticmethod
def format_playwright_error(error):
# apparently, playwright Error.stack contains all the info that we
# want: exception name, message and stacktrace. The docs say that
# error.stack is optional, so fallback to the standard repr if it's
# unavailable.
return error.stack or str(error)
class JsMultipleErrors(Exception):
"""
This is raised in case we get multiple JS errors in the page
"""
def __init__(self, errors):
lines = ["Multiple JS errors found:"]
for err in errors:
lines.append(JsError.format_playwright_error(err))
msg = "\n".join(lines)
super().__init__(msg)
self.errors = errors
class ConsoleMessageCollection:
"""
Helper class to collect and expose ConsoleMessage in a Pythonic way.
Usage:
console.log.messages: list of ConsoleMessage with type=='log'
console.log.lines: list of strings
console.log.text: the whole text as single string
console.debug.* same as above, but with different types
console.info.*
console.error.*
console.warning.*
console.all.* same as above, but considering all messages, no filters
"""
class View:
"""
Filter console messages by the given msg_type
"""
def __init__(self, console, msg_type):
self.console = console
self.msg_type = msg_type
@property
def messages(self):
if self.msg_type is None:
return self.console._messages
else:
return [
msg for msg in self.console._messages if msg.type == self.msg_type
]
@property
def lines(self):
return [msg.text for msg in self.messages]
@property
def text(self):
return "\n".join(self.lines)
_COLORS = {
"error": "red",
"warning": "brown",
}
def __init__(self, logger):
self.logger = logger
self._messages = []
self.all = self.View(self, None)
self.log = self.View(self, "log")
self.debug = self.View(self, "debug")
self.info = self.View(self, "info")
self.error = self.View(self, "error")
self.warning = self.View(self, "warning")
def add_message(self, msg):
# log the message: pytest will capute the output and display the
# messages if the test fails.
category = f"console.{msg.type}"
color = self._COLORS.get(msg.type)
self.logger.log(category, msg.text, color=color)
self._messages.append(msg)
class Logger:
"""
Helper class to log messages to stdout.
Features:
- nice formatted category
- keep track of time passed since the last reset
- support colors
NOTE: the (lowercase) logger fixture is defined in conftest.py
"""
def __init__(self):
self.reset()
# capture things like [pyscript/main]
self.prefix_regexp = re.compile(r"(\[.+?\])")
def reset(self):
self.start_time = time.time()
def colorize_prefix(self, text, *, color):
# find the first occurrence of something like [pyscript/main] and
# colorize it
start, end = Color.escape_pair(color)
return self.prefix_regexp.sub(rf"{start}\1{end}", text, 1)
def log(self, category, text, *, color=None):
delta = time.time() - self.start_time
text = self.colorize_prefix(text, color="teal")
line = f"[{delta:6.2f} {category:15}] {text}"
if color:
line = Color.set(color, line)
print(line)
class Color:
"""
Helper method to print colored output using ANSI escape codes.
"""
black = "30"
darkred = "31"
darkgreen = "32"
brown = "33"
darkblue = "34"
purple = "35"
teal = "36"
lightgray = "37"
darkgray = "30;01"
red = "31;01"
green = "32;01"
yellow = "33;01"
blue = "34;01"
fuchsia = "35;01"
turquoise = "36;01"
white = "37;01"
@classmethod
def set(cls, color, string):
start, end = cls.escape_pair(color)
return f"{start}{string}{end}"
@classmethod
def escape_pair(cls, color):
try:
color = getattr(cls, color)
except AttributeError:
pass
start = f"\x1b[{color}m"
end = "\x1b[00m"
return start, end

View File

@@ -0,0 +1,222 @@
import textwrap
import pytest
from playwright import sync_api
from .support import JsError, JsMultipleErrors, PyScriptTest
class TestSupport(PyScriptTest):
"""
These are NOT tests about PyScript.
They test the PyScriptTest class, i.e. we want to ensure that all the
testing machinery that we have works correctly.
"""
def test_basic(self):
"""
Very basic test, just to check that we can write, serve and read a simple
HTML (no pyscript yet)
"""
doc = """
<html>
<body>
<h1>Hello world</h1>
</body>
</html>
"""
self.writefile("mytest.html", doc)
self.goto("mytest.html")
content = self.page.content()
assert "<h1>Hello world</h1>" in content
def test_console(self):
"""
Test that we capture console.log messages correctly.
"""
doc = """
<html>
<body>
<script>
console.log("my log 1");
console.debug("my debug");
console.info("my info");
console.error("my error");
console.warn("my warning");
console.log("my log 2");
</script>
</body>
</html>
"""
self.writefile("mytest.html", doc)
self.goto("mytest.html")
assert len(self.console.all.messages) == 6
assert self.console.all.lines == [
"my log 1",
"my debug",
"my info",
"my error",
"my warning",
"my log 2",
]
# fmt: off
assert self.console.all.text == textwrap.dedent("""
my log 1
my debug
my info
my error
my warning
my log 2
""").strip()
# fmt: on
assert self.console.log.lines == ["my log 1", "my log 2"]
assert self.console.debug.lines == ["my debug"]
def test_check_errors(self):
doc = """
<html>
<body>
<script>throw new Error('this is an error');</script>
</body>
</html>
"""
self.writefile("mytest.html", doc)
self.goto("mytest.html")
with pytest.raises(JsError) as exc:
self.check_errors()
# check that the exception message contains the error message and the
# stack trace
msg = str(exc.value)
assert "Error: this is an error" in msg
assert f"at {self.http_server}/mytest.html" in msg
#
# after a call to check_errors, the errors are cleared
self.check_errors()
def test_check_errors_multiple(self):
doc = """
<html>
<body>
<script>throw new Error('error 1');</script>
<script>throw new Error('error 2');</script>
</body>
</html>
"""
self.writefile("mytest.html", doc)
self.goto("mytest.html")
with pytest.raises(JsMultipleErrors) as exc:
self.check_errors()
assert "error 1" in str(exc.value)
assert "error 2" in str(exc.value)
#
# check that errors are cleared
self.check_errors()
def test_clear_errors(self):
doc = """
<html>
<body>
<script>throw new Error('this is an error');</script>
</body>
</html>
"""
self.writefile("mytest.html", doc)
self.goto("mytest.html")
self.clear_errors()
# self.check_errors does not raise, because the errors have been
# cleared
self.check_errors()
def test_wait_for_console(self):
"""
Test that self.wait_for_console actually waits.
If it's buggy, the test will try to read self.console.log BEFORE the
log has been written and it will fail.
"""
doc = """
<html>
<body>
<script>
setTimeout(function() {
console.log('Page loaded!');
}, 100);
</script>
</body>
</html>
"""
self.writefile("mytest.html", doc)
self.goto("mytest.html")
# we use a timeout of 500ms to give plenty of time to the page to
# actually run the setTimeout callback
self.wait_for_console("Page loaded!", timeout=200)
assert self.console.log.lines[-1] == "Page loaded!"
def test_wait_for_console_exception_1(self):
"""
Test that if a JS exception is raised while waiting for the console, we
report the exception and not the timeout.
There are two main cases:
1. there is an exception and the console message does not appear
2. there is an exception but the console message appears anyway
This test checks for case 1. Case 2 is tested by
test_wait_for_console_exception_2
"""
# case 1: there is an exception and the console message does not appear
doc = """
<html>
<body>
<script>throw new Error('this is an error');</script>
</body>
</html>
"""
self.writefile("mytest.html", doc)
# "Page loaded!" will never appear, of course.
self.goto("mytest.html")
with pytest.raises(JsError) as exc:
self.wait_for_console("Page loaded!", timeout=200)
assert "this is an error" in str(exc.value)
assert isinstance(exc.value.__context__, sync_api.TimeoutError)
#
# if we use check_errors=False, the error are ignored, but we get the
# Timeout anyway
self.goto("mytest.html")
with pytest.raises(sync_api.TimeoutError):
self.wait_for_console("Page loaded!", timeout=200, check_errors=False)
# we still got a JsError, so we need to manually clear it, else the
# test fails at teardown
self.clear_errors()
def test_wait_for_console_exception_2(self):
"""
See the description in test_wait_for_console_exception_1.
"""
# case 2: there is an exception, but the console message appears
doc = """
<html>
<body>
<script>
setTimeout(function() {
console.log('Page loaded!');
}, 100);
throw new Error('this is an error');
</script>
</body>
</html>
"""
self.writefile("mytest.html", doc)
self.goto("mytest.html")
with pytest.raises(JsError) as exc:
self.wait_for_console("Page loaded!", timeout=200)
assert "this is an error" in str(exc.value)
#
# with check_errors=False, the Error is ignored and the
# wait_for_console succeeds
self.goto("mytest.html")
self.wait_for_console("Page loaded!", timeout=200, check_errors=False)
# clear the errors, else the test fails at teardown
self.clear_errors()

View File

@@ -0,0 +1,53 @@
import re
from .support import PyScriptTest
class TestBasic(PyScriptTest):
def test_pyscript_hello(self):
self.pyscript_run(
"""
<py-script>
print('hello pyscript')
</py-script>
"""
)
# this is a very ugly way of checking the content of the DOM. If we
# find ourselves to write a lot of code in this style, we will
# probably want to write a nicer API for it.
inner_html = self.page.locator("py-script").inner_html()
pattern = r'<div id="py-.*">hello pyscript</div>'
assert re.search(pattern, inner_html)
def test_execution_in_order(self):
"""
Check that they py-script tags are executed in the same order they are
defined
"""
self.pyscript_run(
"""
<py-script>import js; js.console.log('one')</py-script>
<py-script>js.console.log('two')</py-script>
<py-script>js.console.log('three')</py-script>
<py-script>js.console.log('four')</py-script>
"""
)
assert self.console.log.lines == [
self.PY_COMPLETE,
"one",
"two",
"three",
"four",
]
def test_escaping_of_angle_brackets(self):
"""
Check that py-script tags escape angle brackets
"""
self.pyscript_run(
"""
<py-script>import js; js.console.log(1<2, 1>2)</py-script>
<py-script>js.console.log("<div></div>")</py-script>
"""
)
assert self.console.log.lines == [self.PY_COMPLETE, "true false", "<div></div>"]

View File

@@ -0,0 +1,36 @@
from .support import PyScriptTest
class TestAsync(PyScriptTest):
def test_multiple_async(self):
self.pyscript_run(
"""
<py-script>
import js
import asyncio
for i in range(3):
js.console.log('A', i)
await asyncio.sleep(0.1)
</py-script>
<py-script>
import js
import asyncio
for i in range(3):
js.console.log('B', i)
await asyncio.sleep(0.1)
js.console.log("async tadone")
</py-script>
"""
)
self.wait_for_console("async tadone")
assert self.console.log.lines == [
"Python initialization complete",
"A 0",
"B 0",
"A 1",
"B 1",
"A 2",
"B 2",
"async tadone",
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

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