├── .github └── workflows │ ├── on_commit_do_all_unittests.yml │ ├── on_commit_do_deploy_mkdocs.yml │ ├── on_tag_do_release_and_deploy_pypi.yml │ ├── selenium.yaml │ ├── test_platforms.yml │ └── update_version.py ├── .gitignore ├── .idx ├── dev.nix └── icon.png ├── .vscode ├── settings.json └── tasks.json ├── LICENSE ├── README.md ├── _pyscript_dev.html ├── brython ├── README.md ├── bryted.html ├── example1.html ├── example2.html └── htag.txt ├── docs ├── README.md ├── calling_an_event.md ├── concepts.md ├── creating_a_tag.md ├── hrenderer.md ├── htag.png ├── htag.svg ├── index.md ├── js_bidirectionnal.md ├── query_params.md ├── runner.md ├── runners.md ├── tag_update.md ├── tuto_create_an_input_component.md └── tutorial.md ├── examples ├── 7guis │ ├── README.md │ ├── gui1.py │ ├── gui2.py │ ├── gui3.py │ ├── gui4.py │ └── gui5.py ├── README.md ├── ace_editor.py ├── app.py ├── autoreload.py ├── bulma.py ├── calc.py ├── camshot.py ├── demo.py ├── hello_world.py ├── htag_with_state_manager.py ├── leaflet.py ├── matplot.py ├── navigate_with_hashchange.py ├── new_timer.py ├── pyscript.html ├── pyscript_demo.html ├── pyscript_htagui.html ├── pyscript_htbulma.html ├── pyscript_matplotlib.html ├── pyscript_with_hashchange.html ├── secretapp.py ├── stream.py └── todomvc.py ├── htag ├── __init__.py ├── __main__.py ├── attrs.py ├── render.py ├── runners │ ├── __init__.py │ ├── chromeappmode.py │ ├── commons │ │ └── __init__.py │ ├── pyscript.py │ ├── pywebview.py │ ├── runner.py │ └── server │ │ └── __init__.py ├── tag.py └── ui.py ├── manual_tests_base.py ├── manual_tests_event_0.100.1.py ├── manual_tests_events.py ├── manual_tests_expose.py ├── manual_tests_htbulma.py ├── manual_tests_new_InternalCall.py ├── manual_tests_persitent.py ├── manual_tests_qp.py ├── manual_tests_remove.py ├── mkdocs.yml ├── old_runners ├── README.md ├── androidapp.py ├── browserhttp.py ├── browserstarlettehttp.py ├── browserstarlettews.py ├── browsertornadohttp.py ├── chromeapp.py ├── devapp.py └── winapp.py ├── pyproject.toml ├── selenium ├── app1.py ├── app2.py ├── app3.py ├── app4.py ├── app_all_bindings.py ├── hclient.py ├── run.py └── tests.py ├── test_attrs.py ├── test_callbacks.py ├── test_constructors.py ├── test_dom.py ├── test_init_render.py ├── test_interactions.py ├── test_main.py ├── test_new_events.py ├── test_placeholder.py ├── test_renderer.py ├── test_runners.py ├── test_session_persitent.py ├── test_simple.py ├── test_states_guesser.py ├── test_statics.py ├── test_tag_tech.py └── test_update.py /.github/workflows/on_commit_do_all_unittests.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Run tests 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | python-version: [ 3.8, 3.9, "3.10","3.11","3.12" ] 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Set up Python ${{ matrix.python-version }} 22 | uses: actions/setup-python@v2 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | - name: Install dependencies 26 | run: | 27 | python -m pip install --upgrade pip 28 | python -m pip install pytest pytest-asyncio 29 | python -m pip install uvicorn starlette tornado kivy pywebview fake-winreg 30 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 31 | if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi 32 | - name: Test with pytest 33 | run: | 34 | pytest . 35 | -------------------------------------------------------------------------------- /.github/workflows/on_commit_do_deploy_mkdocs.yml: -------------------------------------------------------------------------------- 1 | name: Publish mkdocs pages 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | build: 9 | name: Deploy docs 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout main 13 | uses: actions/checkout@v2 14 | 15 | - name: Deploy docs 16 | uses: mhausenblas/mkdocs-deploy-gh-pages@master 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | CONFIG_FILE: mkdocs.yml 20 | EXTRA_PACKAGES: build-base 21 | -------------------------------------------------------------------------------- /.github/workflows/on_tag_do_release_and_deploy_pypi.yml: -------------------------------------------------------------------------------- 1 | name: "On Tag -> Deploy a release to pypi" 2 | 3 | #TODO: in the future, try to do that : https://mestrak.com/blog/semantic-release-with-python-poetry-github-actions-20nn 4 | 5 | on: 6 | push: 7 | tags: 8 | - 'v*.*.*' 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Set up Python 16 | uses: actions/setup-python@v4 17 | with: 18 | python-version: '3.10' 19 | 20 | - name: Patch Sources with "tag version ref" 21 | run: python .github/workflows/update_version.py ${{github.ref_name}} 22 | 23 | - name: Install dependencies # could be done by poetry ;-) 24 | run: | 25 | python -m pip install --upgrade pip 26 | python -m pip install pytest pytest-asyncio 27 | python -m pip install uvicorn starlette tornado kivy pywebview fake-winreg 28 | 29 | - name: Test htag pytests 30 | run: | 31 | python3 -m pytest . 32 | 33 | - name: Create htag artifact from source versionned 34 | uses: actions/upload-artifact@v4 35 | with: 36 | name: htag_release_sources_${{github.ref_name}} 37 | path: | 38 | htag/* 39 | 40 | - name: Build and publish to pypi 41 | uses: JRubics/poetry-publish@v1.12 42 | with: 43 | pypi_token: ${{ secrets.PYPI_TOKEN }} 44 | 45 | -------------------------------------------------------------------------------- /.github/workflows/selenium.yaml: -------------------------------------------------------------------------------- 1 | # taken from https://github.com/jsoma/selenium-github-actions 2 | name: Selenium GUI Tests 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | # every day at 3:00 AM 7 | - cron: '0 3 * * *' 8 | jobs: 9 | selenium: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | app: [app1, app2, app3, app4, app_all_bindings] 14 | steps: 15 | - name: Check out this repo 16 | uses: actions/checkout@v3 17 | - name: Set up Python 18 | uses: actions/setup-python@v4 19 | with: 20 | python-version: '3.11' 21 | - name: Installed package list 22 | run: apt list --installed 23 | - name: Remove Chrome 24 | run: sudo apt purge google-chrome-stable 25 | - name: Remove default Chromium 26 | run: sudo apt purge chromium-browser 27 | - name: Install a new Chromium 28 | run: sudo apt install -y chromium-browser 29 | - name: Install selenium/poetry packages 30 | run: pip install webdriver-manager selenium poetry 31 | - name: pip list 32 | run: pip list 33 | 34 | 35 | ############################################################################# 36 | ## test the basic Runner (WS MODE) 37 | ############################################################################# 38 | - name: Run Tests Runner/WS ${{ matrix.app }} 39 | run: | 40 | python selenium/run.py WS ${{ matrix.app }} & 41 | python selenium/tests.py 8000 ${{ matrix.app }} 42 | 43 | ############################################################################# 44 | ## test the basic Runner (HTTP MODE) 45 | ############################################################################# 46 | - name: Run Tests Runner/HTTP ${{ matrix.app }} 47 | run: | 48 | python selenium/run.py HTTP ${{ matrix.app }} & 49 | python selenium/tests.py 8000 ${{ matrix.app }} 50 | 51 | ############################################################################# 52 | ## test with PyScript Runner 53 | ############################################################################# 54 | - name: Build WHL for pyscript tests 55 | run: poetry build 56 | 57 | - name: Run Tests PyScript (can't exit itself) ${{ matrix.app }} 58 | run: | 59 | python selenium/run.py PyScript ${{ matrix.app }} 8001 & 60 | python selenium/tests.py 8001 ${{ matrix.app }} 61 | killall python 62 | 63 | -------------------------------------------------------------------------------- /.github/workflows/test_platforms.yml: -------------------------------------------------------------------------------- 1 | name: test platforms 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | ci: 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | # python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] 12 | # os: [ubuntu-latest, macos-latest, windows-latest] 13 | python-version: ["3.9"] 14 | os: [windows-latest] 15 | poetry-version: ["1.2.1"] 16 | runs-on: ${{ matrix.os }} 17 | steps: 18 | - uses: actions/checkout@v2 19 | - uses: actions/setup-python@v2 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | - name: Run image 23 | uses: abatilo/actions-poetry@v2 24 | with: 25 | poetry-version: ${{ matrix.poetry-version }} 26 | - name: poetry run pytest 27 | run: | 28 | poetry lock 29 | poetry install 30 | poetry run pytest 31 | -------------------------------------------------------------------------------- /.github/workflows/update_version.py: -------------------------------------------------------------------------------- 1 | import sys,re 2 | 3 | 4 | def patch_init(v): 5 | file="htag/__init__.py" 6 | content = re.sub(r'__version__ = [^#]*',f'__version__ = "{v}" ',open(file,'r+').read(),1) 7 | assert v in content 8 | with open(file,'w+') as fid: 9 | fid.write( content ) 10 | return file 11 | 12 | def patch_pyproject(v): 13 | file="pyproject.toml" 14 | content = re.sub(r'version = [^#]*',f'version = "{v}" ',open(file,'r+').read(),1) 15 | assert v in content 16 | with open(file,'w+') as fid: 17 | fid.write( content ) 18 | return file 19 | 20 | if __name__=="__main__": 21 | v=sys.argv[1] 22 | assert v.lower().startswith("v"), "version should start with 'v' (was '%s')" %v 23 | assert v.count(".")==2, "version is not semver (was '%s')" %v 24 | version=v[1:] # remove 'v' 25 | f1=patch_init(version) 26 | f2=patch_pyproject(version) 27 | print(f"Files '{f1}' & '{f2}' updated to version '{version}' !") -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | AEFF* 132 | -------------------------------------------------------------------------------- /.idx/dev.nix: -------------------------------------------------------------------------------- 1 | # To learn more about how to use Nix to configure your environment 2 | # see: https://developers.google.com/idx/guides/customize-idx-env 3 | { pkgs, ... }: { 4 | # Which nixpkgs channel to use. 5 | channel = "stable-23.11"; # or "unstable" 6 | 7 | # Use https://search.nixos.org/packages to find packages 8 | packages = [ 9 | pkgs.poetry 10 | pkgs.python311Packages.pytest 11 | # pkgs.go 12 | # pkgs.python311 13 | # pkgs.python311Packages.pip 14 | # pkgs.nodejs_20 15 | # pkgs.nodePackages.nodemon 16 | ]; 17 | 18 | # Sets environment variables in the workspace 19 | env = {}; 20 | idx = { 21 | # Search for the extensions you want on https://open-vsx.org/ and use "publisher.id" 22 | extensions = [ 23 | # "vscodevim.vim" 24 | ]; 25 | 26 | # Enable previews 27 | previews = { 28 | enable = true; 29 | previews = { 30 | # web = { 31 | # # Example: run "npm run dev" with PORT set to IDX's defined port for previews, 32 | # # and show it in IDX's web preview panel 33 | # command = ["npm" "run" "dev"]; 34 | # manager = "web"; 35 | # env = { 36 | # # Environment variables to set for your server 37 | # PORT = "$PORT"; 38 | # }; 39 | # }; 40 | }; 41 | }; 42 | 43 | # Workspace lifecycle hooks 44 | workspace = { 45 | # Runs when a workspace is first created 46 | onCreate = { 47 | # Example: install JS dependencies from NPM 48 | # npm-install = 'npm install'; 49 | }; 50 | onStart = { 51 | # Example: start a background task to watch and re-build backend code 52 | # watch-backend = "npm run watch-backend"; 53 | }; 54 | }; 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /.idx/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manatlan/htag/13b4aa77173b9036594b49c761b66c45720abf91/.idx/icon.png -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.testing.pytestArgs": [ 3 | ".","-s","-v" 4 | ], 5 | "python.testing.unittestEnabled": false, 6 | "python.testing.pytestEnabled": true, 7 | "editor.wordWrap": "off", 8 | "python.linting.pylintEnabled": true, 9 | "python.linting.enabled": true, 10 | 11 | "python.analysis.diagnosticSeverityOverrides": { 12 | "reportUnusedExpression": "none", 13 | }, 14 | "IDX.aI.enableInlineCompletion": true, 15 | "IDX.aI.enableCodebaseIndexing": true, 16 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "Test Coverage", 8 | "type": "shell", 9 | "command": "python3 -m pytest --cov-report html --cov=htag . && google-chrome htmlcov/index.html", 10 | "problemMatcher": [], 11 | "presentation": { 12 | "panel": "new", 13 | "focus": true 14 | } 15 | }, 16 | { 17 | "label": "clean repo", 18 | "type": "shell", 19 | "command": "rm -rf build __pycache__ htag/__pycache__ htag/runners/__pycache__ .pytest_cache .coverage htmlcov", 20 | "problemMatcher": [], 21 | "presentation": { 22 | "panel": "new", 23 | "focus": true 24 | } 25 | }, 26 | { 27 | "label": "Build zip with all", 28 | "type": "shell", 29 | "command": "rm -rf build __pycache__ htag/__pycache__ htag/runners/__pycache__ .pytest_cache .coverage htmlcov; zip -r src.zip htag examples test_*", 30 | "problemMatcher": [], 31 | "presentation": { 32 | "panel": "new", 33 | "focus": true 34 | } 35 | }, 36 | ] 37 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 manatlan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /_pyscript_dev.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 19 | 20 | 21 | loading (recent/2024) pyscript ;-) 22 | 23 | 24 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /brython/README.md: -------------------------------------------------------------------------------- 1 | # HTag for Brython 2 | 3 | [Brython](https://brython.info/) is a marvelous implementation of py3 in javascript, and here is an **htag.Tag** implem for brython. 4 | The goal of this side htag project, is to provide a brython's htag way to create components whose could be compatibles with **htag** and **brython**. 5 | 6 | In fact, it's just It's a class helper, to facilitate the creation of html element. 7 | 8 | In this repo, you will find : 9 | 10 | - [htag.txt](https://github.com/manatlan/htag/blob/main/brython/htag.txt) : the minimal implementation (to use with brython) 11 | - `htagfull.txt` : a more complete implem ... **when it will be ready** ;-) 12 | - somes examples 13 | - [bryted](https://raw.githack.com/manatlan/htag/main/brython/bryted.html) : an online editor to test brython'htag components (made with brython'htag) **PREVERSION** 14 | 15 | 16 | ## Instructions 17 | Put this line in your html file : 18 | ```html 19 | 20 | ``` 21 | In a ` 10 | 15 | 16 | 17 | 18 | 31 | 32 | 33 | 34 | 35 | 77 | 78 |
79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /brython/example2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Example 2 5 | 6 | 7 | 10 | 15 | 16 | 24 | 26 | 27 | 28 | 29 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | See docs here : 2 | https://manatlan.github.io/htag/ 3 | -------------------------------------------------------------------------------- /docs/concepts.md: -------------------------------------------------------------------------------- 1 | 2 | # Concepts 3 | 4 | Here are the concepts, for 1.0 version. (but all is here since 0.8.15) 5 | (For now, some concepts are presented in [tutorial](../tutorial). But here, it's my global plan to make a minimal doc ;-) ) 6 | 7 | ## Technically 8 | **HTag** let you easily build UI elements/widgets (using HTML technologies under the hood), and display them in any things which can render HTML (thru [runners](../runners)). 9 | 10 | In fact, it will render it as a SPA, and will manage all interactions for you (between the UI and the python code), without much knowledgement in html/js/css techs. 11 | 12 | Everything is done in 3 layers : 13 | 14 | * **`Tag`**: the module to build your UI (it's a metaclass to build html/htag components) 15 | * `HRenderer` : the abstraction layer between `Tag` (^) and `Runner` (v) (which make all the magic). You can forget it. 16 | * **`Runner`** : the process which will run the **main tag** (AKA the **htag app**) in a context. 17 | 18 | The `Runner` will manage the underlying communications between the UI (the front (python htag > html)), and the python code (the back (python)), in differents manners, depending on the `Runner` which is used. (ex: Some use HTTP only, some use HTTP or WS, some use inproc/direct calls ... depending of the technologies used by the `Runner`) 19 | 20 | There are a lot of [runners](../runners), which comes OOTB with [htag](https://pypi.org/project/htag/). Just grab the one which suit your needs. All you need to know is how to build a GUI App with the `htag.Tag`, and it's the purpose of this page ;-) 21 | 22 | But the main concept is here: you can developp an **Htag App** which will work anywhere; as a web app, as a desktop app, or as an android app : it will be the same codebase (just depending on the `Runner` context) 23 | 24 | It can be seen as a "VueJs|Angular|React" framework on python side; a statefull instance which manage/render htag's components (states changes are handled by the interactions between the front and the back) 25 | 26 | ## Tag construction 27 | 28 | `htag.Tag` is a metaclass helper to build an html tag. 29 | 30 | ```python 31 | from htag import Tag 32 | 33 | mydiv = Tag.div("hello world") 34 | ``` 35 | `mydiv` will produce a html ouptut as `
hello world
` ;-) 36 | 37 | The firt parameter is the default content of the object, and can be everything (which have a `__str__` method) 38 | 39 | Here are some others tag constructions : 40 | 41 | ``` python 42 | Tag.div(42) # -->
42
43 | Tag.div() # -->
44 | Tag.div( "hello " + Tag.b("world") ) # -->
hello world
45 | Tag.what_you_want("hello") # --> hello 46 | ``` 47 | . . . 48 | 49 | - TODO: (and inherit open/closed (the `**a` trick)) 50 | - TODO: (placeholder) 51 | 52 | ## Tag properties : parent and root 53 | - TODO: (warn root in init !) 54 | - TODO: (strict mode) 55 | 56 | ## Run javascript 57 | - TODO: [@expose decorator](js_bidirectionnal.md) 58 | - TODO: self.js vs self.call( Js ) (and tag js var) 59 | 60 | ## Bind events 61 | - TODO: (four ways, chaining bind, chaining Js before/after) 62 | - TODO: (use b'' for javascript) 63 | 64 | ## Events (in python side) 65 | - TODO: sync/async and yield 66 | - TODO: and stream (adding tag with yield) 67 | 68 | ## Include statics 69 | - TODO: howto, and shortcuts for js/style 70 | - TODO: and imports trick 71 | 72 | ## rendering lately vs dynamic 73 | - TODO: main.tag setted as body 74 | - TODO: in render : avoid tag construction -> coz redraw all (now protected in STRICT_MODE) 75 | - TODO: ... And hrenderer/runners (and url queryparams) 76 | 77 | ## Runners 78 | [url and instanciations](query_params.md) 79 | - TODO: (and state management) 80 | - For now, [See runners](../runners) 81 | -------------------------------------------------------------------------------- /docs/hrenderer.md: -------------------------------------------------------------------------------- 1 | # HRenderer 2 | 3 | It's the class which handle an htag.Tag class, and make it lives in a runner context. It exposes methods for a [runner](runners.md). 4 | 5 | In general, you don't need to use this class on your own. In general, you will use a 'runner' which will do it for you. If you are 6 | here, you will to create your own runner, or see how it works. 7 | 8 | definitions: 9 | 10 | * hr : means the HRrenderer instance 11 | * runner : it's a high level class (which use a hrenderer under-the-hood) to make the magix. 12 | 13 | 14 | ## `def __init__(self, tagClass: type, js:str, exit_callback:Optional[Callable]=None, init= ((),{}), fullerror=False, statics=[], session=None ):` 15 | 16 | It's the constructor of an instance ;-) 17 | 18 | When you got your hr instance : `str(hr)` will (always) contain a full html page which should work OOTB in a client-side ! It's often 19 | called the "1st rendering". 20 | 21 | ### tagClass [htag.Tag class] 22 | It's the subclass, which will be instanciate in the hr instance. 23 | 24 | ### js [str] 25 | It's a string which define the JS methods to interact with the hr instance, and the way to "start" the live 26 | of the html page in client side (by calling a js 'start()' method). 27 | 28 | For a http runner, it's often like that: 29 | ```python 30 | async function interact( o ) { 31 | action( await (await window.fetch("/",{method:"POST", body:JSON.stringify(o)})).text() ) 32 | } 33 | 34 | window.addEventListener('DOMContentLoaded', start ); 35 | ``` 36 | 37 | For a websocket runner, it's often like that: 38 | 39 | ```python 40 | async function interact( o ) { 41 | ws.send( JSON.stringify(o) ); 42 | } 43 | var ws = new WebSocket("ws://"+document.location.host+"/ws"); 44 | ws.onopen = start; 45 | ws.onmessage = function(e) { 46 | action( e.data ); 47 | }; 48 | ``` 49 | 50 | There are 3 main ideas, in this js str : 51 | 52 | * provide a js 'interact' method, which will pass its arguments, to python (back side) 53 | * provide the way to call the `action( )` from the python response (back side) 54 | * define how to call the 'start()' method 55 | 56 | Remarks 57 | - start() and action( ) are js methods provided by the "1st rendering" 58 | - as you can see : 59 | - The http form is synchronous (the action is executed with the return of the interact) 60 | - The ws form is asynchronous (the action is executed when a message is sended from the hr to the client side) 61 | 62 | 63 | ### init [tuple] 64 | it's the parameters which will be used to initialize the htag.Tag class, in the form (*args, **kargs). 65 | 66 | If it can't instanciate the class with the init parameters, it will try to instanciate it with null parameters ( aka `( (), {} )`). 67 | 68 | ### fullerror [boolean] 69 | it's a boolean to prevent the hr to send back just the error, or the full stacktrace. 70 | 71 | ### statics [List] 72 | It's a list of "statics", which will be added in the html>head page of the 1st rendering. 73 | 74 | ### session [dict] 75 | It's a dict which will hold the session for the user. It got only sense in the "web runners" (those ones manage a session/dict by user). All others runners are mono-user, 76 | so the session dict will only be a empty dict. 77 | 78 | ### exit_callback [method] 79 | It's a method, which will be called when a user call the tag.exit() method. It got only sense in runners which are mono-user (because it will quit the app). The "web runners" 80 | dont implement this feature. 81 | 82 | 83 | ## `async def interact(self, id, method_name:str, args, kargs, event=None) -> dict:` 84 | 85 | It's the python method, which must be called thru the 'js' declaration in the hr constructor, to make an "interaction" 86 | with the tag instance, managed by the hr. 87 | 88 | In general, you won't need to pass arguments to it because : you just cable the js call 89 | 90 | Under the hood, this method returns 'actions' (dict), to redraw the client side, and to execute some js. 91 | ... 92 | 93 | 94 | **TODO** I find it ambiguous ... because the reality is simpler ! 95 | 96 | -------------------------------------------------------------------------------- /docs/htag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manatlan/htag/13b4aa77173b9036594b49c761b66c45720abf91/docs/htag.png -------------------------------------------------------------------------------- /docs/htag.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 38 | HTML5 Logo 40 | 45 | 50 | 52 | 53 | 55 | HTML5 Logo 56 | 57 | 58 | 59 | # 70 | 71 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # HTag 2 | 3 | 4 | 5 | ... Docs are coming (2024/03/1) ... ;-) 6 | 7 | Meanwhile, you can learn a lot on [htag's demo](https://htag.glitch.me/) ... or better [try/repl your self](https://raw.githack.com/manatlan/htag/main/examples/pyscript_demo.html) 8 | (it's an htag app, running with the runner PyScript, in a simple html page, which provide examples in an html editor) ;-) 9 | 10 | 11 | ## Quick start 12 | 13 | Just pip the **htag** lib, from [pypi.org](https://pypi.org/project/htag/) 14 | 15 | ```bash 16 | $ pyhon3 -m pip install htag -U 17 | ``` 18 | 19 | Create a starter app ;-) 20 | 21 | ```bash 22 | $ pyhon3 -m htag 23 | ``` 24 | (it will create a "main.py" basic htag app) 25 | 26 | Start the app : 27 | 28 | ```bash 29 | $ pyhon3 -m htag main.py 30 | ``` 31 | 32 | It will run the app, in a "basic UI" (open a tab in your default browser), in "developping mode" ("hot reload" while pressing F5/refresh, and htag errors popups in a nice message ) 33 | 34 | IRL, you should define your [Runner](runner.md) to not launch in "developping mode" ! Edit your "main.py" like that : 35 | 36 | ```python 37 | ... 38 | if __name__=="__main__": 39 | Runner(App,interface=(640,480)).run() 40 | ``` 41 | 42 | And run it like that : 43 | 44 | ```bash 45 | $ pyhon3 main.py 46 | ``` 47 | 48 | Note : 49 | 50 | - REAL MODE, no "developping mode", pressing F5 will not recreate instances (the app), and "errors" will be ignored silently in UI. 51 | - here, it will run the "UI part" in a "chrome app mode" (healess chrome) (you'll need to have chrome installed, but it will fallback to a normal tab in default browser if not. 52 | - The [Runner](runner.md) got a lot of options ;-) 53 | - There are more [Runners](runners.md). But : 54 | 55 | - if you want to make a "web app" (for many users) you should use [htagweb](https://github.com/manatlan/htagweb) 56 | - if you want to make an Android (smarthpone/TV) app, you should use [htagapk](https://github.com/manatlan/htagapk) 57 | 58 | 59 | Now, you can continue on [tutorial](tutorial.md) ;-) 60 | 61 | ## Concept 62 | 63 | You can see it like a python way to create apps, which can use the best of python world, and the best of html/javascript world. And best of all, you can **easily** create apps (same codebase!) that will work in desktop, android & web world (& html only too (thansk to [PyScript](https://manatlan.github.io/htag/runners/#pyscript) )) . 64 | 65 | The concept is simple : you create UI python classes, which inherits from `htag.Tag.` (which nativly render html/js/css, and provides minimal dom manipulation api). You can craft components, by reusing other components. Your main component is called the **htag app**. 66 | 67 | And you run your **htag app**, with a [htag.runner](runners.md) ... (which will create an instance of it) ... And htag (the renderer part) will manage **interactions** between the client side and the python side. At each states change in python side, the rendering changes are done on client side. The 2 sides are keeped synchronous ! It only redraws components which has changed ! 68 | 69 | Of course, htag components can (or must) reuse htag components, and will enforce you to create them, to separate your UI logics... and build your own set of reusable components ! That's why htag is more taggued as an "UI Toolkit to build UI tookit", than an "UI toolkit to build GUI". 70 | 71 | The (far from) perfect example is [htbulma](https://github.com/manatlan/htbulma), which is a set of "ready-to-use htag components", to build GUI from ground. It uses **htag**, and provide UI components, usable without (too many)knowledgement of html/js/css world. 72 | 73 | 74 | -------------------------------------------------------------------------------- /docs/js_bidirectionnal.md: -------------------------------------------------------------------------------- 1 | # JS bidirectionnal (@expose) 2 | 3 | Sometimes, when using a JS lib, with heavy/bidirectionnal interactions : you 'll need to call js and receive events from JS. You can do that by using a `@expose` decorator. 4 | 5 | Here is a "audio player" tag, which expose a python "play' method, and a "event" method (whil will be decorated) to receive event from the js side. Here is the best way to do it : 6 | 7 | ```python 8 | from htag import Tag, expose 9 | 10 | class APlayer(Tag.div): 11 | statics=Tag.script(_src="https://cdnjs.cloudflare.com/ajax/libs/howler/2.2.4/howler.min.js") 12 | 13 | def init(self): 14 | self.js=""" 15 | 16 | self.play= function(url) { 17 | if(this._hp) { 18 | this._hp.stop(); 19 | this._hp.unload(); 20 | } 21 | 22 | this._hp = new Howl({ 23 | src: [url], 24 | autoplay: true, 25 | loop: false, 26 | volume: 1, 27 | onend: function() { 28 | self.event("end",url); 29 | } 30 | }); 31 | 32 | } 33 | """ % self.bind.event(b"args") 34 | 35 | def play(self,url): 36 | self.call( f"self.play(`{url}`)" ) 37 | 38 | @expose 39 | def event(self,name,url): 40 | self+=f"EVENT: {name} {url}" 41 | ``` 42 | -------------------------------------------------------------------------------- /docs/query_params.md: -------------------------------------------------------------------------------- 1 | # Instanciating main htag class with url/query params 2 | 3 | In most runners, you can use "query parameters" (from the url) to instanciate your htag main class. 4 | 5 | Admit that your main class looks like that : 6 | 7 | ```python 8 | 9 | class MyTag(Tag.div): 10 | def init(self,name,age=12): 11 | ... 12 | 13 | ``` 14 | 15 | If can point your running App to urls like: 16 | 17 | * `/?name=john&age=12` -> will create the instance `MyTag("john",'12')` 18 | * `/?Jim&42` -> will create the instance `MyTag("Jim",'42')` 19 | * `/?Jo` -> will create the instance `MyTag("Jo",12)` 20 | 21 | As long as parameters, fulfill the signature of the constructor : it will construct the instance with them. 22 | If it doesn't fit : it try to construct the instance with no parameters ! 23 | 24 | So things like that : 25 | 26 | * `/` -> will result in http/400, **because `name` is mandatory** in all cases !! 27 | 28 | BTW, if your class looks like that (with `**a` trick, to accept html attributes or property instance) 29 | 30 | ```python 31 | class MyTag(Tag.div): 32 | def init(self,name,age=12,**a): 33 | ... 34 | ``` 35 | things like that, will work : 36 | 37 | * `/?Jim&_style=background:red` -> will create the instance `MyTag("Jim",'42')`, and change the default bg color of the instance. 38 | 39 | So, it's a **best practice**, to not have the `**a` trick in the constructor of main htag class (the one which is runned by the runner) 40 | 41 | **Remarks:** 42 | 43 | * all query params are string. It's up to you to cast to your needs. 44 | * this feature comes with htag >= 0.8.0 45 | 46 | ## Try by yourself 47 | 48 | ```python 49 | from htag import Tag 50 | 51 | class MyApp(Tag.div): 52 | def init(self,param="default",**a): 53 | self <= f"Param: {param}" 54 | aa=lambda x: Tag.a("test: "+x,_href=x,_style="display:block") 55 | self <= aa("?") 56 | self <= aa("?rien") 57 | self <= aa("?toto") 58 | self <= aa("?toto&56") 59 | self <= aa("?param=kiki") 60 | self <= aa("?nimp=kaka") 61 | self <= aa("?tot1#a1") 62 | self <= aa("?tot2#a2") 63 | self <= aa("?to12&p=12") 64 | self <= aa("?tot3&_style=background:red") 65 | 66 | from htag.runners import Runner 67 | Runner(MyApp).run() 68 | ``` 69 | 70 | Currently, this feature works in all htag.runners except PyWebView ;-( 71 | -------------------------------------------------------------------------------- /docs/runners.md: -------------------------------------------------------------------------------- 1 | # Runners 2 | 3 | The 'Runners' is the htag denomination, for some classes provided with htag, to help you to run a **Htag App** in a context. Here are the current provided runners, and could give you ideas of what you want ;-) 4 | 5 | **htag** provides officialy 3 runners : 6 | 7 | - Runner : the base one, for desktop app 8 | - PyScript : the special to be runned in pure html side 9 | - PyWebView : a specific for desktop using a CEF UI (using [pywebview](https://pywebview.flowrl.com/)) ... (it could be in another htag module soon) 10 | 11 | If you want to create an "Android app" (smartphone, tv, etc ...) see [htagapk recipes](https://github.com/manatlan/htagapk)) 12 | 13 | If you want to create an "Web app" (multiple clients) see [htagweb runner](https://github.com/manatlan/htagweb)) 14 | 15 | Between 0.90 & 1.0 versions, htag provides the old runners (for compatibility reasons), but they are deprecated and will be removed at 1.0. Here they are: 16 | 17 | - AndroidApp 18 | - BrowserHTTP 19 | - BrowserStarletteHTTP 20 | - BrowserStarletteWS 21 | - ChromeApp 22 | - DevApp 23 | - BrowserTornadoHTTP 24 | - WinApp 25 | 26 | Currently, all are faked/simulated and use the new `Runner` instead (thus, a runner like BrowserStarletteWS, doesn't use starlette anymore, but the new runner home-made server, which is enough robust for one client/user ;-) ) 27 | 28 | 29 | 30 | 31 | 32 | ## Runner 'Runner' 33 | 34 | This runner can simulate all old runners. All specialized features, that were in some runners only, are all available now. This runner is a pure python server (holding Websocket/HTTP connexions). Things like uvicorn/starlette/tornado were overbloated for a server which can handle one client ;-) 35 | 36 | See [Runner](runner.md) 37 | 38 | ## Runner 'PyScript' 39 | Run everything in client side, thanks to the marvellous [pyscript](https://pyscript.net/). Don't know if there is an utility, but it's possible ;-). 40 | It should run OOTB, everywhere where pyscript runs. 41 | 42 | Run your `App` (htag.Tag class), in a HTML file, like this : 43 | 44 | ```python 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | packages = ["htag"] 53 | 54 | 55 | loading pyscript ;-) 56 | 57 | ############################################################################### 58 | from htag import Tag 59 | 60 | class App(Tag.body): 61 | ... 62 | 63 | ############################################################################### 64 | from htag.runners import PyScript 65 | PyScript( App ).run() 66 | 67 | 68 | 69 | 70 | ``` 71 | 72 | [source](https://github.com/manatlan/htag/blob/main/htag/runners/pyscript.py) 73 | 74 | **Pros** 75 | 76 | - you only need a browser ;-) 77 | - Interactions are INPROC. 78 | - no need of external libs 79 | 80 | 81 | **Cons** 82 | 83 | - Launching the pyscript environnement can be long. 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | ## Runner 'PyWebView' 98 | Run everything in a [pywebview](https://pywebview.flowrl.com/) instance. The ideal solution to provide a "python GUI app". 99 | 100 | Run your `App` (htag.Tag class) like this : 101 | 102 | ```python 103 | from htag.runners import PyWebView 104 | PyWebView( App ).run() 105 | ``` 106 | 107 | 108 | [source](https://github.com/manatlan/htag/blob/main/htag/runners/pywebview.py) 109 | 110 | **Pros** 111 | 112 | - Interactions are INPROC. 113 | - the app can `self.exit()` 114 | 115 | 116 | **Cons** 117 | 118 | - til pywebview [doesn't support async calls](https://github.com/r0x0r/pywebview/issues/867), full htag features (async) will not be available ;-( 119 | - need external libs 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /docs/tag_update.md: -------------------------------------------------------------------------------- 1 | # tag.update() 2 | 3 | This feature add a "send from backend to frontend" capacity, which can only work with 4 | [Runners](runners.md) which have a "permanent connexion" with front end (think websocket) 5 | 6 | Note that this feature will not work, in this cases: 7 | 8 | - When the Runner is in `http_only` mode (because this feature use websocket only) ! 9 | - With [PyWebView](runners.md#PyWebView) runner, because [pywebview](https://pywebview.flowrl.com) doesn't support async things. 10 | 11 | It opens a lot of powers, and real time exchanges (see `examples/new_*.py`) ! and works for [htagweb](https://github.com/manatlan/htagweb) & [htagapk](https://github.com/manatlan/htagapk) too. 12 | 13 | ## Use cases 14 | 15 | `tag.update()` is a coroutine, so you should use it in a coroutine only. It returns True/False depending 16 | on the capacity to update in realtime. (with http's runners : it will always return False) 17 | 18 | Here is an async method of an htag component (runned with a `asyncio.ensure_future( self.do_something() )`): 19 | ```python 20 | 21 | async def do_something(self): 22 | self += "hello" 23 | isUpdatePossible = await self.update() 24 | 25 | ``` 26 | 27 | Note that, the "first updates" can return False, while the websocket is not really connected. And will return 28 | True when it's done. 29 | -------------------------------------------------------------------------------- /examples/7guis/README.md: -------------------------------------------------------------------------------- 1 | This is attempts to do the https://eugenkiss.github.io/7guis/ with HTAG. 2 | 3 | There are 7 tasks : 4 | - 1) counter : done 5 | - 2) Temp Converter : done 6 | - 3) Flight Booker : done 7 | - 4) Timer : **IN PROGRESS** 8 | - 5) CRUD : done 9 | - 6) Circle Drawer : **TODO** 10 | - 7) Cells : **TODO** 11 | 12 | It's not complete yet. 13 | -------------------------------------------------------------------------------- /examples/7guis/gui1.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from htag import Tag # the only thing you'll need ;-) 3 | 4 | 5 | class Gui1(Tag.body): 6 | 7 | def init(self): 8 | self.value=0 9 | 10 | def render(self): 11 | self.clear() 12 | self <= Tag.Span( self.value ) 13 | self <= Tag.Button( "+", _onclick=self.bind.inc() ) 14 | 15 | def inc(self): 16 | self.value+=1 17 | 18 | App=Gui1 19 | if __name__=="__main__": 20 | # and execute it in a pywebview instance 21 | from htag.runners import * 22 | PyWebWiew( Gui1 ).run() 23 | 24 | # here is another runner, in a simple browser (thru ajax calls) 25 | # BrowserHTTP( Page ).run() 26 | -------------------------------------------------------------------------------- /examples/7guis/gui2.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from htag import Tag # the only thing you'll need ;-) 3 | 4 | 5 | class Gui2(Tag.body): 6 | 7 | def init(self): 8 | self.valueC=0 9 | self.valueF=0 10 | 11 | def render(self): 12 | self.clear() 13 | self <= Tag.div( Tag.Input( _value=self.valueC, _onchange=self.bind.changeC(b"this.value") ) +"C" ) 14 | self <= Tag.div( Tag.Input( _value=self.valueF, _onchange=self.bind.changeF(b"this.value") ) +"F") 15 | 16 | def changeC(self,v): 17 | self.valueC=float(v) 18 | self.valueF=(self.valueC*9/5) - 32 19 | 20 | def changeF(self,v): 21 | self.valueF=float(v) 22 | self.valueC=(self.valueF-32) *(5/9) 23 | 24 | 25 | 26 | App=Gui2 27 | if __name__=="__main__": 28 | # and execute it in a pywebview instance 29 | from htag.runners import * 30 | PyWebWiew( Gui2 ).run() 31 | 32 | # here is another runner, in a simple browser (thru ajax calls) 33 | # BrowserHTTP( Page ).run() 34 | -------------------------------------------------------------------------------- /examples/7guis/gui3.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from htag import Tag # the only thing you'll need ;-) 3 | 4 | 5 | class Gui3(Tag.body): 6 | 7 | def init(self): 8 | self.selected = "One Way" 9 | 10 | def render(self): 11 | options = [Tag.option(i,_selected=(self.selected==i)) for i in ["One Way","Return Flight"]] 12 | 13 | self.clear() 14 | self <= Tag.Select( options, _onchange=self.bind.setSelected(b"this.value") ) 15 | self <= Tag.input(_type="date") 16 | self <= Tag.input(_type="date",_disabled=self.selected=="One Way") 17 | 18 | def setSelected(self,v): 19 | self.selected = v 20 | 21 | 22 | 23 | 24 | App=Gui3 25 | if __name__=="__main__": 26 | # and execute it in a pywebview instance 27 | from htag.runners import * 28 | PyWebWiew( Gui3 ).run() 29 | 30 | # here is another runner, in a simple browser (thru ajax calls) 31 | # BrowserHTTP( Page ).run() 32 | -------------------------------------------------------------------------------- /examples/7guis/gui4.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from htag import Tag # the only thing you'll need ;-) 3 | 4 | class Percent(Tag.div): 5 | def init(self,v): 6 | self.value=v 7 | self["style"]="width:100px;height:20px;border:1px solid black" 8 | def render(self): 9 | self <= Tag.div(_style="height:20px;background:blue;width:%s%%;" % self.value) 10 | 11 | 12 | class Gui4(Tag.body): 13 | """ https://eugenkiss.github.io/7guis/tasks/#timer """ 14 | """ https://svelte.dev/examples/7guis-timer """ 15 | 16 | #TODO: finnish it 17 | #TODO: finnish it 18 | #TODO: finnish it 19 | #TODO: finnish it 20 | #TODO: finnish it 21 | 22 | def init(self): 23 | self.value=10 24 | self.gauge = Percent(20) 25 | 26 | def render(self): 27 | self.clear() 28 | self <= self.gauge 29 | 30 | self <= Tag.input( 31 | _value=self.value, 32 | _type="range", 33 | _min=1, 34 | _max=100, 35 | _step=1, 36 | _onchange=self.bind.change(b"this.value") 37 | ) 38 | self<=Tag.button("Reset") 39 | 40 | def change(self,v): 41 | self.value=v 42 | 43 | self.gauge.value=v # FOR TEST ONLY 44 | 45 | 46 | App=Gui4 47 | if __name__=="__main__": 48 | # and execute it in a pywebview instance 49 | from htag.runners import * 50 | PyWebWiew( Gui4 ).run() 51 | 52 | # here is another runner, in a simple browser (thru ajax calls) 53 | # BrowserHTTP( Page ).run() 54 | -------------------------------------------------------------------------------- /examples/7guis/gui5.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from htag import Tag # the only thing you'll need ;-) 3 | 4 | 5 | class Gui5(Tag.body): 6 | """ https://eugenkiss.github.io/7guis/tasks#crud """ 7 | 8 | def init(self): 9 | self.db=["albert","franz","fred"] 10 | self.selected=None 11 | self.filter="" 12 | self.name = "" 13 | 14 | def render(self): 15 | options = [Tag.option(i,_value=idx,_selected=idx==self.selected) for idx,i in enumerate(self.db) if self.filter in i] 16 | 17 | self <= Tag.Input( _value=self.filter, _onchange=self.bind.changeFilter(b"this.value"),_placeholder="filter" ) 18 | self <= Tag.br() 19 | self <= Tag.select( options, _size=10, _onchange=self.bind.setSelected(b"this.value") ,_style="width:80px") 20 | self <= Tag.Input( _value=self.name, _onchange=self.bind.changeName(b"this.value") ) 21 | self <= Tag.hr() 22 | self <= Tag.Button( "create", _onclick=self.bind.create() ) 23 | self <= Tag.Button( "delete", _onclick=self.bind.delete() ) 24 | self <= Tag.Button( "update", _onclick=self.bind.update() ) 25 | 26 | def setSelected(self,v): 27 | self.selected = int(v) 28 | self.name = self.db[self.selected] 29 | 30 | def changeFilter(self,v): 31 | self.filter = v 32 | 33 | def changeName(self,v): 34 | self.name = v 35 | 36 | def create(self): 37 | self.db.append(self.name) 38 | 39 | def delete(self): 40 | if self.selected is not None: 41 | del self.db[self.selected] 42 | self.selected=None 43 | 44 | def update(self): 45 | if self.selected is not None: 46 | self.db[self.selected] = self.name 47 | 48 | 49 | App=Gui5 50 | if __name__=="__main__": 51 | # and execute it in a pywebview instance 52 | from htag.runners import * 53 | PyWebWiew( Gui5 ).run() 54 | 55 | # here is another runner, in a simple browser (thru ajax calls) 56 | # BrowserHTTP( Page ).run() 57 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | Since **htag** can run in **pyscript**, you can execute this 5 examples, in your browser ;-) 2 | 3 | - [pyscript.html](https://raw.githack.com/manatlan/htag/main/examples/pyscript.html), a simple and minimal example 4 | - [pyscript_demo.html](https://raw.githack.com/manatlan/htag/main/examples/pyscript_demo.html), a simple editor to test its own htag's Tags, in a pyscript context. 5 | - [pyscript_with_hashchange.html](https://raw.githack.com/manatlan/htag/main/examples/pyscript_with_hashchange.html), an example using a component which listening hash event, and react on hashchange (can use navigation history) 6 | - [pyscript_htbulma.html](https://raw.githack.com/manatlan/htag/main/examples/pyscript_htbulma.html), which use [htbulma](https://github.com/manatlan/htbulma) **DEPRECATED**, a lib of pre-made htag's components. 7 | - [pyscript_htagui.html](https://raw.githack.com/manatlan/htag/main/examples/pyscript_htagui.html), which use [htagui](https://github.com/manatlan/htagui), a lib of pre-made htag's components. 8 | - [pyscript_matplotlib.html](https://raw.githack.com/manatlan/htag/main/examples/pyscript_matplotlib.html), an example using **matplotlib** (which works in pyscript too) ;-) 9 | 10 | All theses things, will work in a simple html page (no (real) python required) 11 | -------------------------------------------------------------------------------- /examples/ace_editor.py: -------------------------------------------------------------------------------- 1 | import os,sys; sys.path.insert(0,os.path.dirname(os.path.dirname(__file__))) 2 | 3 | from htag import Tag 4 | import html 5 | 6 | class Ed(Tag.div): 7 | """ A class which embed the ace editor (python syntax) """ 8 | 9 | statics = [ 10 | Tag.script(_src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.14/ace.js"), 11 | Tag.script(_src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.14/mode-python.js"), 12 | Tag.script(_src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.14/theme-cobalt.js"), 13 | Tag.script(_src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.14/ext-searchbox.js"), 14 | Tag.script(_src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.14/ext-language_tools.js"), 15 | ] 16 | 17 | def __init__(self,value,width="100%",height="100%",mode="python",onsave=None): 18 | self.value = value 19 | super().__init__(_style="width:%s;height:%s;" % (width,height)) 20 | placeholder="myed%s" % id(self) 21 | 22 | oed= Tag.div(self.value,_style="width:100%;height:100%;min-height:20px;") 23 | self <= oed 24 | self.onsave=onsave 25 | 26 | self.js = """ 27 | tag.ed=ace.edit( "%s" ); 28 | tag.ed.setTheme("ace/theme/cobalt"); 29 | tag.ed.session.setMode("ace/mode/%s"); 30 | tag.ed.session.setUseWrapMode(false); 31 | tag.ed.setOptions({"fontSize": "12pt"}); 32 | tag.ed.setBehavioursEnabled(false); 33 | tag.ed.session.setUseWorker(false); 34 | tag.ed.getSession().setUseSoftTabs(true); 35 | 36 | function commandSave () {%s} 37 | 38 | tag.ed.commands.addCommand({ 39 | name: "commandSave", 40 | bindKey: {"win": "Ctrl-S", "mac": "Command-S"}, 41 | readOnly: "True", 42 | exec: commandSave, 43 | }) 44 | """ % (id(oed),mode, self.bind._save( b"tag.ed.getValue()")) 45 | 46 | def _save(self,value): 47 | self.value = value 48 | if self.onsave: self.onsave(self) 49 | 50 | 51 | class App(Tag.body): 52 | """ Using a component which embed the ace editor """ 53 | statics=[Tag.style(""" 54 | html, body {width:100%;height:100%;margin:0px} 55 | """)] 56 | imports=Ed # IRL, this line is not needed 57 | 58 | def init(self): 59 | self.e1 = Ed("text1", onsave=self.maj) 60 | self.e2 = Ed("text2", onsave=self.maj) 61 | self <= Tag.div( 62 | self.e1+self.e2, 63 | _style="height:100px;display:flex;gap:4px;" 64 | ) 65 | self.txt=Tag.pre("Press CTRL+S in each Editor") 66 | self <= self.txt 67 | 68 | def maj(self,o): 69 | self.txt.clear( f"{html.escape(repr(o))} saved -> '{o.value}'" ) 70 | 71 | 72 | from htag.runners import DevApp as Runner # need starlette+uvicorn !!! 73 | #from htag.runners import BrowserHTTP as Runner 74 | app=Runner(App) 75 | if __name__=="__main__": 76 | app.run() 77 | -------------------------------------------------------------------------------- /examples/app.py: -------------------------------------------------------------------------------- 1 | import os,sys; sys.path.insert(0,os.path.dirname(os.path.dirname(__file__))) 2 | from htag import Tag 3 | 4 | """ 5 | example, mainly for my visual tests 6 | 7 | 8 | """ 9 | 10 | # "https://cdn.jsdelivr.net/npm/bulma@0.8.2/css/bulma.min.css" 11 | # css=Tag.style("""/*! bulma.io v0.8.2 | MIT License | github.com/jgthms/bulma *""") 12 | 13 | css=Tag.link( _href="https://cdn.jsdelivr.net/npm/bulma@0.8.2/css/bulma.min.css",_rel="stylesheet") 14 | 15 | class CptWithStars(Tag.div): 16 | statics=css 17 | 18 | def init(self,value=0): 19 | self.nb=value 20 | 21 | def render(self): # special method 22 | self.clear() 23 | self.add( Tag.button( "-",_onclick=self.bind.onclick(-1) )) 24 | self.add( self.nb ) 25 | self.add( Tag.button( "+",_onclick=self.bind.onclick(1) )) 26 | for i in range(self.nb): 27 | self.add("*") 28 | 29 | def onclick(self,v): 30 | self.nb+=v 31 | 32 | 33 | class Cpt(Tag.div): 34 | statics=css 35 | 36 | def init(self,value=0): 37 | self.nb=value 38 | 39 | self.ocpt = Tag.span( value ) 40 | 41 | self += Tag.button( "-",_onclick=self.bind.onclick(-1) ) 42 | self += self.ocpt 43 | self += Tag.button( "+",_onclick=self.bind.onclick(1) ) 44 | 45 | def onclick(self,v): 46 | self.nb+=v 47 | self.ocpt.clear(self.nb) 48 | 49 | import time,asyncio 50 | 51 | class Page(Tag.body): 52 | statics=css,b"window.error=function(txt) {document.body.innerHTML += txt}","body {background:#CCC}" 53 | 54 | def init(self): 55 | self.c1=CptWithStars(0) 56 | self.c2=Cpt() 57 | 58 | self.add( self.c1 ) 59 | self.add( self.c2 ) 60 | self.add( Tag.button("alert",_onclick="alert(document.querySelector('input').value)",_class="button") ) 61 | 62 | self.t=Tag.input(_value="",_onchange=self.bind.press(b"this.value")) 63 | self.add( self.t ) 64 | 65 | s=Tag.div() 66 | s<= Tag.button("BUG JS pre",_onclick=self.bind.bugjs(txt=b"gfdsfsgfds()"),_class="button") 67 | s<= Tag.button("BUG JS post",_onclick=self.bind.bugjs(),_class="button") 68 | s<= Tag.button("BUG PY (normal)",_onclick=self.bind.bugpy(),_class="button") 69 | s<= Tag.button("BUG PY (gen)",_onclick=self.bind.bugpysg(),_class="button") 70 | s<= Tag.button("BUG PY (async gen)",_onclick=self.bind.bugpyag(),_class="button") 71 | self <= s 72 | 73 | self.add( Tag.button("Sync Yield",_onclick=self.bind.testSYield(),_class="button") ) 74 | self.add( Tag.button("ASync Yield",_onclick=self.bind.testAYield(),_class="button") ) 75 | self.add( Tag.button("exit",_onclick=lambda o: self.exit(),_class="button") ) 76 | 77 | self.js="console.log(42)" 78 | 79 | def press(self,v): 80 | self.t["value"]=v 81 | print(v) 82 | 83 | def testSYield(self): 84 | for i in list("ABCDEF"): 85 | self.call(f"console.log('{i}')") 86 | time.sleep(0.5) 87 | print(i) 88 | self.add( i ) 89 | yield 90 | 91 | async def testAYield(self): 92 | for i in list("ABCDEF"): 93 | self.call(f"console.log('{i}')") 94 | await asyncio.sleep(0.5) 95 | print(i) 96 | self.add( i ) 97 | yield 98 | 99 | def bugpy(self): 100 | a=12/0 101 | async def bugpyag(self): 102 | yield 103 | a=12/0 104 | yield 105 | def bugpysg(self): 106 | yield 107 | a=12/0 108 | yield 109 | def bugjs(self,txt=""): 110 | self.call("fgdsgfd()") 111 | 112 | 113 | # exemple for instanciating the logging before ... 114 | 115 | # from htag.runners import BrowserHTTP as Runner 116 | # from htag.runners import DevApp as Runner 117 | # from htag.runners import PyWebView as Runner 118 | # from htag.runners import BrowserStarletteHTTP as Runner 119 | # from htag.runners import BrowserStarletteWS as Runner 120 | # from htag.runners import BrowserTornadoHTTP as Runner 121 | # from htag.runners import AndroidApp as Runner 122 | # from htag.runners import ChromeApp as Runner 123 | from htag.runners import WinApp as Runner 124 | App=Page 125 | 126 | r=Runner( Page ) 127 | if __name__=="__main__": 128 | import logging 129 | logging.basicConfig(format='[%(levelname)-5s] %(name)s: %(message)s',level=logging.DEBUG) 130 | # logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',level=logging.DEBUG) 131 | 132 | logging.getLogger("htag.tag").setLevel( logging.WARNING ) 133 | r.run() 134 | 135 | -------------------------------------------------------------------------------- /examples/autoreload.py: -------------------------------------------------------------------------------- 1 | import os,sys; sys.path.insert(0,os.path.dirname(os.path.dirname(__file__))) 2 | 3 | from htag import Tag 4 | 5 | """ 6 | AUTORELOAD is possible with this 2 runners : BrowserStarletteHTTP & BrowserStarletteWS 7 | (could be very handy in development phase) 8 | 9 | See this example, on how to instanciate the runner and use uvicorn/autoreload 10 | """ 11 | 12 | class Demo(Tag.div): 13 | statics=[ Tag.style("""body {background:#EEE}""") ] 14 | 15 | def init(self): 16 | for c in range(2000000,16581375,10000): 17 | self <= Tag.button("Hi",_style=f"background:#{hex(c)[2:]}") 18 | 19 | ############################################################################################# 20 | App=Demo 21 | from htag.runners import * 22 | 23 | app = BrowserStarletteHTTP( Demo ) 24 | # app = BrowserStarletteWS( Demo ) 25 | 26 | if __name__=="__main__": 27 | import uvicorn 28 | uvicorn.run("autoreload:app",host="127.0.0.1",port=8000,reload=True) 29 | 30 | ## or the classic : 31 | #app.run() -------------------------------------------------------------------------------- /examples/calc.py: -------------------------------------------------------------------------------- 1 | import os,sys; sys.path.insert(0,os.path.dirname(os.path.dirname(__file__))) 2 | 3 | from htag import Tag 4 | 5 | """ 6 | This example show you how to make a "Calc App" 7 | (with physical buttons + keyboard events) 8 | 9 | There is no work for rendering the layout ;-) 10 | 11 | Can't be simpler ! 12 | 13 | """ 14 | 15 | class Calc(Tag.div): 16 | statics=[Tag.style(""" 17 | .mycalc *,button {font-size:2em;font-family: monospace} 18 | """)] 19 | 20 | def init(self): 21 | self.txt="" 22 | self.aff = Tag.Div(" ",_style="border:1px solid black") 23 | 24 | self["class"]="mycalc" 25 | self <= self.aff 26 | self <= Tag.button("C", _onclick=self.bind( self.clean) ) 27 | self <= [Tag.button(i, _onclick=self.bind( self.press, i) ) for i in "0123456789+-x/."] 28 | self <= Tag.button("=", _onclick=self.bind( self.compute ) ) 29 | 30 | #-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/ with real keyboard 31 | self["onkeyup"] = self.presskey 32 | 33 | def presskey(self,ev): 34 | key=ev.key 35 | if key in "0123456789+-*/.": 36 | self.press(key) 37 | elif key=="Enter": 38 | self.compute() 39 | elif key in ["Delete","Backspace"]: 40 | self.clean() 41 | #-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/ 42 | 43 | def press(self,val): 44 | self.txt += val 45 | self.aff.clear( self.txt ) 46 | 47 | def compute(self): 48 | try: 49 | self.txt = str(eval(self.txt.replace("x","*"))) 50 | self.aff.clear( self.txt ) 51 | except: 52 | self.txt = "" 53 | self.aff.clear( "Error" ) 54 | 55 | def clean(self): 56 | self.txt="" 57 | self.aff.clear(" ") 58 | 59 | App=Calc 60 | 61 | if __name__=="__main__": 62 | # import logging 63 | # logging.basicConfig(format='[%(levelname)-5s] %(name)s: %(message)s',level=logging.DEBUG) 64 | # logging.getLogger("htag.tag").setLevel( logging.INFO ) 65 | 66 | # and execute it in a pywebview instance 67 | from htag.runners import * 68 | 69 | # here is another runner, in a simple browser (thru ajax calls) 70 | BrowserHTTP( Calc ).run() 71 | # PyWebWiew( Calc ).run() 72 | -------------------------------------------------------------------------------- /examples/camshot.py: -------------------------------------------------------------------------------- 1 | import os,sys; sys.path.insert(0,os.path.dirname(os.path.dirname(__file__))) 2 | from htag import Tag 3 | 4 | class Cam(Tag.video): 5 | """ Htag component to start the cam, and to take screnshot (by clicking on it)""" 6 | 7 | # some javascripts needed for the component 8 | statics = Tag.script(""" 9 | function startCam(video) { 10 | navigator.mediaDevices.getUserMedia({ 11 | video: true, 12 | audio: false 13 | }) 14 | .then(function(stream) { 15 | video.srcObject = stream; 16 | video.play(); 17 | }) 18 | .catch(function(err) { 19 | const o = document.createElement("div"); 20 | o.innerHTML = "NO CAMERA: " + err; 21 | video.replaceWith(o); 22 | }); 23 | } 24 | 25 | function takeCamShot(video) { 26 | let {width, height} = video.srcObject.getTracks()[0].getSettings(); 27 | 28 | let canvas = document.createElement("canvas"); 29 | let context = canvas.getContext('2d'); 30 | canvas.width = width; 31 | canvas.height = height; 32 | context.drawImage(video, 0, 0, canvas.width, canvas.height); 33 | return canvas.toDataURL('image/jpeg'); 34 | } 35 | """) 36 | 37 | # js which will be executed, each time the component is appended 38 | js = """startCam(tag);""" 39 | 40 | def init(self,callback=None,width=300,height=300): 41 | self.width=width 42 | self.height=height 43 | self["style"]=f"width:{width}px;height:{height}px;border:1px solid black" 44 | if callback: 45 | self["onclick"]=self.bind( callback, b"takeCamShot(this)" ) 46 | 47 | class App(Tag.body): 48 | """ An Htag App to handle the camera, screenshots are displayed in the flow""" 49 | 50 | statics = b"function error(m) {alert(m)}" 51 | 52 | def init(self): 53 | self <= Cam(self.takeShot) 54 | 55 | def takeShot(self,o,dataurl): 56 | self <= Tag.img(_src=dataurl,_style="max-width:%spx;max-height:%spx;" % (o.width,o.height)) 57 | 58 | 59 | 60 | if __name__=="__main__": 61 | from htag.runners import * 62 | # r=PyWebWiew( App ) 63 | # r=BrowserStarletteHTTP( App ) 64 | # r=BrowserStarletteWS( App ) 65 | # r=BrowserHTTP( App ) 66 | r=BrowserTornadoHTTP( App ) 67 | r.run() 68 | 69 | -------------------------------------------------------------------------------- /examples/demo.py: -------------------------------------------------------------------------------- 1 | import os,sys; sys.path.insert(0,os.path.dirname(os.path.dirname(__file__))) 2 | 3 | from htag import Tag # the only thing you'll need ;-) 4 | 5 | 6 | class Button(Tag.button): # this Tag will be rendered as a """ 8 | 9 | # this allow you to include statics in headers 10 | # (it will be included only once !!!) 11 | statics = [Tag.style("button.my {background:yellow; border:1px solid black; border-radius:4px}")] 12 | 13 | def __init__(self,txt, callback): 14 | super().__init__() 15 | 16 | # we set some html attributs 17 | self["class"]="my" # set @class to "my" 18 | self["onclick"]=self.bind.onclick() # bind a js event on @onclick 19 | # "self.bind.()" is the trick to generate a js interaction 20 | # binded to this component 21 | 22 | self <= txt # put a text into the button 23 | # it's a shortcut for "self.add( txt )" 24 | 25 | self.callback=callback # save the py callback for later use 26 | 27 | def onclick(self): 28 | # this is the event called by the @onclick 29 | # it will call the py callback 30 | self.callback() 31 | 32 | class Star(Tag.div): # it's a div tag 33 | """ This Star component display 2 buttons to decrease/increase a value 34 | (it displays nb x star according the value) 35 | """ 36 | 37 | def __init__(self,value=0): 38 | super().__init__() 39 | self.nb=value 40 | 41 | def inc(self,v): 42 | self.nb+=v 43 | 44 | def render(self): 45 | # here, the representation is built lately 46 | # (during the __str__ rendering) 47 | 48 | # self.clear() 49 | # we add our buttons, binded to its py method 50 | self <= Button( "-", lambda: self.inc(-1) ) 51 | self <= Button( "+", lambda: self.inc(1) ) 52 | 53 | # we draw the stars 54 | self <= "⭐"*self.nb 55 | 56 | 57 | class Page(Tag.body): # define a , but the renderer will force it to in all cases 58 | """ This is the main Tag, it will be rendered as by the htag/renderer """ 59 | 60 | def init(self): 61 | 62 | # here is a list of movies ;-) 63 | self.movies=[ 64 | ("BatMan", Star(5)), 65 | ("Thor", Star(9)), 66 | ("Superman", Star(7)), 67 | ] 68 | 69 | def render(self): 70 | # here, the representation is built lately 71 | # (during the __str__ rendering) 72 | 73 | # self.clear() 74 | 75 | # we put a title 76 | self <= Tag.h1("Best movies ;-)") # here is shortcut to create "

Best movies ;-)

" 77 | # (it works for any html tag you want ;-) 78 | 79 | # and add our stuff, sorted by nb of stars 80 | for name,star in sorted( self.movies, key=lambda x: -x[1].nb ): 81 | self <= Tag.div( [name,star] ) 82 | 83 | App=Page 84 | if __name__== "__main__": 85 | # and execute it in a pywebview instance 86 | from htag.runners import PyWebView 87 | PyWebView( Page ).run() 88 | 89 | # here is another runner, in a simple browser (thru ajax calls) 90 | # BrowserHTTP( Page ).run() 91 | -------------------------------------------------------------------------------- /examples/hello_world.py: -------------------------------------------------------------------------------- 1 | import os,sys; sys.path.insert(0,os.path.dirname(os.path.dirname(__file__))) 2 | 3 | # the simplest htag'app, in the best env to start development (hot reload/refresh) 4 | from htag import Tag 5 | 6 | class App(Tag.body): 7 | def init(self): 8 | self += "Hello World" 9 | 10 | from htag.runners import DevApp as Runner # need starlette+uvicorn !!! 11 | #from htag.runners import BrowserHTTP as Runner 12 | app=Runner(App) 13 | if __name__=="__main__": 14 | app.run() 15 | -------------------------------------------------------------------------------- /examples/htag_with_state_manager.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os,sys; sys.path.insert(0,os.path.dirname(os.path.dirname(__file__))) 3 | 4 | """ 5 | For bigger project ... it's a good practice to start 6 | with an unique "source of truth" (all data in one place). 7 | 8 | Here is a example 9 | 10 | Principes: 11 | - You make a private dict in your htag component -> THE store 12 | (all data will be read/write from here) 13 | - In your component, you only have read access on this store. 14 | - and ONLY yours (inter-)actions can mutate this store 15 | 16 | See pinia/vuex for vuejs, redux for react, or ngrx for angular, etc ... 17 | """ 18 | 19 | from htag import Tag 20 | from dataclasses import dataclass 21 | 22 | # the DB simulation part 23 | #....................................................................... 24 | @dataclass 25 | class Product: 26 | name: str 27 | price: int 28 | 29 | PRODUCTS={ 30 | "ref1":Product("Peach",10), 31 | "ref4":Product("Apple",2), 32 | "ref5":Product("Pear",3), 33 | "ref7":Product("Banana",3), 34 | } 35 | 36 | # a class to provide only readacess to your store (dict) 37 | #....................................................................... 38 | class Store: 39 | def __init__(self, store:dict ): 40 | self.__store = store 41 | def __getitem__(self,k): 42 | return self.__store.get(k) 43 | 44 | 45 | # the components : 46 | #....................................................................... 47 | class PageList(Tag.div): 48 | def init(self): 49 | self <= Tag.h1("Products") 50 | for ref,p in PRODUCTS.items(): 51 | d=Tag.div(_style="border:1px dotted black;display:inline-block;width:100px;height:100px") 52 | d<=Tag.h3(p.name) 53 | d<=Tag.button("View", value=ref, _onclick = lambda o: self.root.action('SELECT',selected=o.value) ) 54 | d<=Tag.button("Add", value=ref,_onclick = lambda o: self.root.action('ADD',selected=o.value) ) 55 | self <= d 56 | 57 | class PageProduct(Tag.div): 58 | def init(self,ref): 59 | p = PRODUCTS[ref] 60 | 61 | b=Tag.button("back", _onclick = lambda o: self.root.action('LISTE') ) 62 | self <= Tag.h1(b+f"Products > {p.name}") 63 | self <= Tag.h3(f"Price: {p.price}€") 64 | self <= Tag.button("Add", _onclick = lambda o: self.root.action('ADD',selected=ref) ) 65 | 66 | class Basket(Tag.div): 67 | 68 | def render(self): # dynamic rendering (so it can react on store changes) 69 | liste = self.root.store["baskets"] 70 | 71 | self.clear() 72 | if liste: 73 | somme=0 74 | for ref in liste: 75 | p = PRODUCTS[ref] 76 | self <= Tag.li( f"{p.name}: {p.price}€" ) 77 | somme+=p.price 78 | self <= Tag.b(f"Total: {somme}€") 79 | self <= Tag.button("clear", _onclick = lambda o: self.root.action('CLEAR') ) 80 | else: 81 | self <= "vide" 82 | 83 | # and your main tag (which will be runned in a runner) 84 | #....................................................................... 85 | 86 | class App(Tag.body): 87 | def init(self): 88 | # the private store 89 | self.__store = {"baskets": []} 90 | 91 | # the public store (read only) 92 | self.store = Store( self.__store ) 93 | 94 | # prepare layout 95 | self.main = Tag() # placeholder ! 96 | 97 | # draw layout 98 | self <= self.main + Tag.div(Basket(),_style="position:fixed;top:0px;right:0px;background:yellow") 99 | 100 | # 1st action 101 | self.action("LISTE") 102 | 103 | def action(self, action, **params): 104 | """ here are the mutations for your actions 105 | The best practice : the store is mutated only in this place ! 106 | """ 107 | if action == "LISTE": 108 | self.main.clear(PageList() ) 109 | elif action == "SELECT": 110 | self.main.clear(PageProduct( params["selected"] ) ) 111 | elif action == "ADD": 112 | self.__store["baskets"].append( params["selected"] ) 113 | elif action == "CLEAR": 114 | self.__store["baskets"] = [] 115 | 116 | print("NEW STORE:",self.__store) 117 | 118 | # the runner part 119 | #....................................................................... 120 | from htag.runners import DevApp as Runner 121 | # from htag.runners import BrowserHTTP as Runner 122 | # from htag.runners import ChromeApp as Runner 123 | 124 | 125 | app=Runner(App) 126 | if __name__=="__main__": 127 | import logging 128 | logging.basicConfig(format='[%(levelname)-5s] %(name)s: %(message)s',level=logging.DEBUG) 129 | 130 | logging.getLogger("htag.tag").setLevel( logging.ERROR ) 131 | logging.getLogger("htag.render").setLevel( logging.ERROR ) 132 | logging.getLogger("uvicorn.error").setLevel( logging.ERROR ) 133 | logging.getLogger("asyncio").setLevel( logging.ERROR ) 134 | app.run() 135 | -------------------------------------------------------------------------------- /examples/leaflet.py: -------------------------------------------------------------------------------- 1 | import os,sys; sys.path.insert(0,os.path.dirname(os.path.dirname(__file__))) 2 | from htag import Tag 3 | 4 | class LeafLet(Tag.div): 5 | statics =[ 6 | Tag.link(_href="https://unpkg.com/leaflet@1.8.0/dist/leaflet.css",_rel="stylesheet"), 7 | Tag.script(_src="https://unpkg.com/leaflet@1.8.0/dist/leaflet.js"), 8 | ] 9 | 10 | def init(self,lat:float,long:float,zoom:int=13): 11 | self["style"]="height:300px;width:300px;border:2px solid black;display:inline-block;margin:2px;" 12 | self.js = f""" 13 | var map = L.map('{id(self)}').setView([{lat}, {long}], {zoom}); 14 | 15 | L.tileLayer('https://{{s}}.tile.openstreetmap.org/{{z}}/{{x}}/{{y}}.png', 16 | {{ 17 | attribution: 'LeafLet', 18 | maxZoom: 17, 19 | minZoom: 9 20 | }}).addTo(map); 21 | """ 22 | 23 | class App(Tag.body): 24 | 25 | def init(self): 26 | self <= LeafLet(51.505, -0.09) #london, uk 27 | self <= LeafLet(42.35,-71.08) #Boston, usa 28 | 29 | self.call( """navigator.geolocation.getCurrentPosition((position)=>{%s});""" 30 | % self.bind.add_me(b"position.coords.latitude",b"position.coords.longitude") 31 | ) 32 | 33 | def add_me(self,lat,long): 34 | self <= Tag.div(f"And you could be near here: ({lat},{long}):") 35 | self <= LeafLet(lat,long) 36 | 37 | 38 | from htag.runners import DevApp 39 | 40 | app=DevApp(App) 41 | 42 | if __name__=="__main__": 43 | app.run() 44 | -------------------------------------------------------------------------------- /examples/matplot.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os,sys; sys.path.insert(0,os.path.dirname(os.path.dirname(__file__))) 3 | 4 | from htag import Tag 5 | import io,base64,random 6 | import matplotlib.pyplot as plt 7 | 8 | class TagPlot(Tag.span): 9 | def init(self,plt): 10 | self["style"]="display:inline-block" 11 | with io.StringIO() as fid: 12 | plt.savefig(fid,format='svg',bbox_inches='tight') 13 | self <= fid.getvalue() 14 | 15 | class App(Tag.body): 16 | def init(self): 17 | self.content = Tag.div() 18 | 19 | # create the layout 20 | self += Tag.h2("MatPlotLib " + Tag.button("Random",_onclick=self.redraw_plt)) 21 | self += self.content 22 | 23 | self.redraw_plt() 24 | 25 | def redraw_plt(self,obj=None): 26 | plt.clf() 27 | plt.ylabel('Some numbers') 28 | plt.xlabel('Size of my list') 29 | my_list=[random.randint(1,10) for i in range(random.randint(20,50))] 30 | plt.plot( my_list ) 31 | 32 | self.content.clear() 33 | self.content += Tag.div(f"My list: {my_list}") 34 | self.content += TagPlot(plt) 35 | 36 | from htag.runners import BrowserHTTP as Runner 37 | app=Runner(App) 38 | if __name__=="__main__": 39 | app.run() 40 | -------------------------------------------------------------------------------- /examples/navigate_with_hashchange.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os,sys; sys.path.insert(0,os.path.dirname(os.path.dirname(__file__))) 3 | 4 | from htag import Tag,expose 5 | 6 | #/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\ 7 | # an htag Tag, to use hashchange events 8 | #/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\ 9 | class View(Tag.div): 10 | def __init__(self,tag=None,**a): # use complex constructor to do complex things ;-) 11 | super().__init__(tag,**a) 12 | self.default = tag 13 | self._refs={} 14 | self.js = """ 15 | if(!window._hashchange_listener) { 16 | window.addEventListener('hashchange',() => {self._hashchange(document.location.hash);}); 17 | window._hashchange_listener=true; 18 | } 19 | """ 20 | @expose 21 | def _hashchange(self,hash): 22 | self.clear( self._refs.get(hash, self.default) ) 23 | 24 | def go(self,tag,anchor=None): 25 | """ Set object 'tag' in the View, and navigate to it """ 26 | anchor=anchor or str(id(tag)) 27 | self._refs[f'#{anchor}'] = tag 28 | self.call( f"document.location=`#{anchor}`") 29 | #/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\ 30 | 31 | class Page1(Tag.span): 32 | def init(self): 33 | self+="Page 1" 34 | 35 | class Page2(Tag.div): 36 | def init(self): 37 | self+="Page 2" 38 | 39 | class App(Tag.body): 40 | """ Example1: classic use (view is just a child of the body), at child level """ 41 | def init(self): 42 | p0 = "welcome" # default page 43 | p1 = Page1() 44 | p2 = Page2() 45 | 46 | self.v = View( p0, _style="border:1px solid red;width:100%;height:400px" ) 47 | 48 | # layout 49 | self += Tag.button("p1",_onclick=lambda o: self.v.go( p1,"p1" )) 50 | self += Tag.button("p2",_onclick=lambda o: self.v.go( p2,"p2" )) 51 | self += self.v 52 | 53 | # class MyApp(View): 54 | # """ Example2: The body is a 'View' (at top level) """ 55 | # def __init__(self,**a): 56 | # p0 = "welcome" # default page 57 | # p1 = Page1() 58 | # p2 = Page2() 59 | 60 | # # add "menus" to pages, to be able to navigate 61 | # sp = lambda o: self.go( o.page, o.page.__class__.__name__ ) 62 | # menus = Tag.button("p1", page=p1, _onclick=sp) + Tag.button("p2",page=p2,_onclick=sp) 63 | # p1 += menus 64 | # p2 += menus 65 | 66 | # super().__init__(p0 + menus,**a) 67 | 68 | 69 | #====================================== 70 | from htag.runners import DevApp as Runner 71 | 72 | app=Runner( App ) 73 | if __name__=="__main__": 74 | app.run() 75 | -------------------------------------------------------------------------------- /examples/new_timer.py: -------------------------------------------------------------------------------- 1 | 2 | # -*- coding: utf-8 -*- 3 | 4 | import os,sys; sys.path.insert(0,os.path.dirname(os.path.dirname(__file__))) 5 | import asyncio,sys,time 6 | 7 | 8 | from htag import Tag 9 | 10 | 11 | class App(Tag.body): 12 | statics="body {background:#EEE;}" 13 | 14 | def init(self): 15 | self.place = Tag.div(js="console.log('I update myself')") 16 | 17 | asyncio.ensure_future( self.loop_timer() ) 18 | 19 | self += "Hello World" + self.place 20 | self+= Tag.button("yo",_onclick=self.doit) 21 | 22 | async def doit(self,o): 23 | self+="x" 24 | 25 | async def loop_timer(self): 26 | while 1: 27 | await asyncio.sleep(0.5) 28 | self.place.clear(time.time() ) 29 | if not await self.place.update(): # update component using current websocket 30 | # break if can't (<- good practice to kill this asyncio/loop) 31 | break 32 | 33 | 34 | 35 | #================================================================================= with update capacity 36 | # from htag.runners import BrowserStarletteWS as Runner 37 | # from htag.runners import ChromeApp as Runner 38 | # from htag.runners import WinApp as Runner 39 | from htag.runners import DevApp as Runner 40 | #================================================================================= 41 | 42 | #~ from htagweb import WebServer as Runner 43 | # from htag.runners import BrowserHTTP as Runner 44 | 45 | app=Runner(App) 46 | 47 | if __name__=="__main__": 48 | app.run() 49 | -------------------------------------------------------------------------------- /examples/pyscript.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Test htag 7 | 8 | 9 | 10 | 11 | loading... 12 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /examples/pyscript_htagui.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Test htag 7 | 8 | 9 | 10 | 11 | loading... 12 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /examples/pyscript_htbulma.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Test htag 7 | 8 | 9 | 10 | 11 | loading... 12 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /examples/pyscript_matplotlib.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Test htag 7 | 8 | 9 | 10 | 11 | loading... 12 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /examples/pyscript_with_hashchange.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Test htag 7 | 8 | 9 | 10 | 11 | loading... 12 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /examples/stream.py: -------------------------------------------------------------------------------- 1 | import os,sys; sys.path.insert(0,os.path.dirname(os.path.dirname(__file__))) 2 | 3 | from htag import Tag # the only thing you'll need ;-) 4 | import asyncio 5 | 6 | """ 7 | This example is for "htag powerusers" ... 8 | If you discovering the project, come back here later ;-) 9 | (just keep in mind, that it's possible to output a large amount of data, in a decent way) 10 | 11 | It shows you the "htag way" to create a component which 12 | can output a large amount of data (outputed from an async source) 13 | without rendering all the object at each yield statement ! 14 | (ex: rendering an http paging, or an apache httpd log ...) 15 | 16 | It use a "htag mechanism" which use the yield to add an object in. 17 | this mechanism is named "stream" 18 | 19 | """ 20 | 21 | async def asyncsource(): 22 | """ this is an async source (which simulate delay to get datas) """ 23 | for i in range(3): 24 | yield "line %s" % i 25 | await asyncio.sleep(0.2) # simulate delay from the input source 26 | 27 | 28 | class Viewer(Tag.ul): 29 | """ Object which render itself using a async generator (see self.feed) 30 | (the content is streamed from an async source) 31 | """ 32 | def __init__(self): 33 | super().__init__(_style="border:1px solid red") 34 | 35 | 36 | async def feed(self): 37 | """ async yield object in self (stream)""" 38 | self.clear() 39 | async for i in asyncsource(): 40 | yield Tag.li(i) # <- automatically added to self instance /!\ 41 | 42 | async def feed_bad(self): 43 | """ very similar (visually), but this way IS NOT GOOD !!!! 44 | because it will render ALL THE OUTPUT at each yield !!!!! 45 | """ 46 | self.clear() 47 | async for i in getdata(): 48 | self <= Tag.li(i) # manually add 49 | yield # and force output all ! 50 | 51 | 52 | class Page(Tag.body): 53 | 54 | def init(self): 55 | 56 | self.view = Viewer() 57 | self <= self.view 58 | 59 | # not good result (yield in others space) 60 | self <= Tag.button( "feed1", _onclick= lambda o: self.view.feed() ) # in the button 61 | self <= Tag.button( "feed2", _onclick= self.bind( lambda o: self.view.feed() ) ) # in Page 62 | 63 | # good result (yield in the viewer) 64 | self <= Tag.button( "feed3", _onclick= self.view.bind( self.view.feed ) ) 65 | self <= Tag.button( "feed4", _onclick= self.view.bind.feed() ) 66 | 67 | App=Page 68 | if __name__=="__main__": 69 | # import logging 70 | # logging.basicConfig(format='[%(levelname)-5s] %(name)s: %(message)s',level=logging.DEBUG) 71 | # logging.getLogger("htag.tag").setLevel( logging.INFO ) 72 | 73 | # and execute it in a pywebview instance 74 | from htag.runners import * 75 | 76 | # here is another runner, in a simple browser (thru ajax calls) 77 | BrowserHTTP( Page ).run() 78 | # PyWebWiew( Page ).run() 79 | -------------------------------------------------------------------------------- /examples/todomvc.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os,sys; sys.path.insert(0,os.path.dirname(os.path.dirname(__file__))) 3 | # see https://metaperl.github.io/pure-python-web-development/todomvc.html 4 | 5 | from htag import Tag 6 | from dataclasses import dataclass 7 | 8 | @dataclass 9 | class Todo: 10 | txt: str 11 | done: bool=False 12 | 13 | class MyTodoListTag(Tag.div): 14 | statics = "label {display:block;cursor:pointer;padding:4px}" 15 | 16 | def init(self): 17 | # init the list 18 | self._list = [] 19 | 20 | # and make a 1st draw 21 | self.redraw() 22 | 23 | def redraw(self): 24 | # clear content 25 | self.clear() 26 | 27 | if self._list: 28 | # if there are todos 29 | 30 | def statechanged(o): 31 | # toggle boolean, using the ref of the instance object 'o' 32 | o.ref.done = not o.ref.done 33 | 34 | # force a redraw (to keep state sync) 35 | self.redraw() 36 | 37 | # we draw the todos with checkboxes 38 | for i in self._list: 39 | self += Tag.label([ 40 | # create a 'ref' attribut on the instance of the input, for the event needs 41 | Tag.input(ref=i,_type="checkbox",_checked=i.done,_onchange=statechanged), 42 | i.txt 43 | ]) 44 | else: 45 | # if no todos 46 | self += Tag.label("nothing to do ;-)") 47 | 48 | def addtodo(self,txt:str): 49 | txt=txt.strip() 50 | if txt: 51 | # if content, add as toto in our ref list 52 | self._list.append( Todo(txt) ) 53 | 54 | # and force a redraw 55 | self.redraw() 56 | 57 | 58 | class App(Tag.body): 59 | statics="body {background:#EEE;}" 60 | 61 | # just to declare that this component will use others components 62 | # (so this one can declare 'statics' from others) 63 | imports=[MyTodoListTag,] # not needed IRL ;-) 64 | 65 | def init(self): 66 | # create an instance of the class 'MyTodoListTag', to manage the list 67 | olist=MyTodoListTag() 68 | 69 | # create a form to be able to add todo, and bind submit event on addtodo method 70 | oform = Tag.form( _onsubmit=olist.bind.addtodo(b"this.q.value") + "return false" ) 71 | oform += Tag.input( _name="q", _type="search", _placeholder="a todo ?") 72 | oform += Tag.Button("add") 73 | 74 | # draw ui 75 | self += Tag.h3("Todo list") + oform + olist 76 | 77 | 78 | 79 | #================================================================================= 80 | # the runner part 81 | #================================================================================= 82 | from htag.runners import BrowserHTTP as Runner 83 | # from htag.runners import DevApp as Runner 84 | # from htag.runners import PyWebView as Runner 85 | # from htag.runners import BrowserStarletteHTTP as Runner 86 | # from htag.runners import BrowserStarletteWS as Runner 87 | # from htag.runners import BrowserTornadoHTTP as Runner 88 | # from htag.runners import AndroidApp as Runner 89 | # from htag.runners import ChromeApp as Runner 90 | # from htag.runners import WinApp as Runner 91 | 92 | app=Runner(App) 93 | if __name__=="__main__": 94 | app.run() 95 | -------------------------------------------------------------------------------- /htag/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # ############################################################################# 3 | # Copyright (C) 2022 manatlan manatlan[at]gmail(dot)com 4 | # 5 | # MIT licence 6 | # 7 | # https://github.com/manatlan/htag 8 | # ############################################################################# 9 | 10 | __version__ = "0.0.0" # auto-updated 11 | 12 | from .tag import Tag,HTagException,expose 13 | from .runners import Runner 14 | 15 | __all__= ["Tag","HTagException","expose","Runner"] 16 | 17 | -------------------------------------------------------------------------------- /htag/__main__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # ############################################################################# 3 | # Copyright (C) 2022 manatlan manatlan[at]gmail(dot)com 4 | # 5 | # MIT licence 6 | # 7 | # https://github.com/manatlan/htag 8 | # ############################################################################# 9 | 10 | import os,sys 11 | 12 | code = """ 13 | # -*- coding: utf-8 -*- 14 | 15 | from htag import Tag 16 | 17 | class App(Tag.body): 18 | statics="body {background:#EEE;}" 19 | 20 | def init(self): 21 | self <= "Hello World" 22 | self <= Tag.button("Say hi", _onclick=self.sayhi) 23 | 24 | def sayhi(self,ev): 25 | self <= "hi!" 26 | 27 | 28 | #================================================================================= 29 | from htag.runners import Runner 30 | 31 | if __name__ == "__main__": 32 | Runner(App).run() 33 | """ 34 | 35 | import argparse 36 | 37 | class BooleanOptionalAction(argparse.Action): 38 | """ only here for compatibility with py < 3.9 """ 39 | def __init__(self, option_strings, dest, default=None, required=False, help=None, **kwargs): 40 | # Créer les options avec et sans 'no' 41 | _option_strings = [] 42 | for option_string in option_strings: 43 | _option_strings.append(option_string) 44 | if option_string.startswith('--'): 45 | _option_strings.append(option_string.replace('--', '--no-')) 46 | 47 | super(BooleanOptionalAction, self).__init__( 48 | option_strings=_option_strings, 49 | dest=dest, 50 | nargs=0, 51 | const=None, 52 | default=default, 53 | required=required, 54 | help=help, 55 | **kwargs 56 | ) 57 | 58 | def __call__(self, parser, namespace, values, option_string=None): 59 | if option_string.startswith('--no-'): 60 | setattr(namespace, self.dest, False) 61 | else: 62 | setattr(namespace, self.dest, True) 63 | 64 | def format_help(self): 65 | return f"{', '.join(self.option_strings)}: {self.help} (default: {self.default})" 66 | 67 | if __name__=="__main__": 68 | 69 | parser = argparse.ArgumentParser( 70 | prog="htag", 71 | description="""Entrypoint to help you or a htag'app. 72 | If a [file] is given: it will try to run it (using dev mode), 73 | else it will create an empty htag file named 'main.py' in current path. Options are just here for the run mode. 74 | """, 75 | ) 76 | parser.add_argument('file', nargs='?', help="if present, the htag'file will be runned (in dev mode)") 77 | parser.add_argument('--host', help='Host listener [default: 127.0.0.1]', default="127.0.0.1") 78 | parser.add_argument('--port', help='Port number [default: 8000]', default="8000") 79 | parser.add_argument('--gui', help="Automatically open interface in a browser [default]",action=BooleanOptionalAction, default=True) 80 | parser.add_argument('--dev', help="Run in dev mode (reload+debug) [default]",action=BooleanOptionalAction, default=True) 81 | args = parser.parse_args() 82 | if args.file: 83 | ########################################################################## 84 | ## run mode 85 | ########################################################################## 86 | htagfile=os.path.realpath(args.file) 87 | try: 88 | assert os.path.isfile(htagfile), f"file '{htagfile}' not found" 89 | from htag.runners import Runner 90 | import importlib.util 91 | module_name=os.path.basename(htagfile)[:-3] 92 | spec = importlib.util.spec_from_file_location(module_name, htagfile) 93 | module = importlib.util.module_from_spec(spec) 94 | sys.modules[module_name] = module 95 | spec.loader.exec_module(module) 96 | 97 | if hasattr(module,"app"): 98 | app=getattr(module,"app") 99 | if isinstance(app,Runner): 100 | print("Found 'app' (new Runner), will run it") 101 | print(app,"Serving") 102 | # run part (like defined in file) 103 | app.run() 104 | sys.exit(0) 105 | 106 | if hasattr(module,"App"): 107 | print("Found 'App' (tag class), will run it") 108 | tagClass=getattr(module,"App") 109 | 110 | # run part (here FULL DEV, and open a tab/browser) 111 | app=Runner(tagClass,reload=args.dev,debug=args.dev,host=args.host,port=args.port, interface=1 if args.gui else 0) 112 | print(app,"Serving") 113 | app.run() 114 | else: 115 | print("ERROR",htagfile,"doesn't contain 'App' (tag class)") 116 | except Exception as e: 117 | print("ERROR",e) 118 | else: 119 | ########################################################################## 120 | ## create mode 121 | ########################################################################## 122 | newfile = "main.py" 123 | 124 | if not os.path.isfile(newfile): 125 | with open(newfile,"w+") as fid: 126 | fid.write(code) 127 | 128 | print("HTag App file created -->", newfile) 129 | else: 130 | print(f"It seems that you've already got a '{newfile}' file") 131 | -------------------------------------------------------------------------------- /htag/attrs.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class StrClass: 4 | """ mimic the js tag.classList """ 5 | def __init__(self,txt=None): 6 | if txt is None: 7 | self._ll=[] 8 | else: 9 | if isinstance(txt,list): 10 | self._ll = list(txt) 11 | else: 12 | self._ll=[i.strip() for i in str(txt).split(" ") if i.strip()] 13 | 14 | def contains(self,c) -> int: 15 | return self._ll.count(c) 16 | 17 | def __contains__(self,kk): 18 | return self.contains(kk)>0 19 | 20 | def add(self,*cc): 21 | for c in cc: 22 | if not self.contains(c): 23 | self._ll.append(c) 24 | 25 | def remove(self,*cc): 26 | for c in cc: 27 | if self.contains(c): 28 | self._ll.remove(c) 29 | 30 | def toggle(self,c): 31 | if c in self._ll: 32 | self.remove(c) 33 | else: 34 | self.add(c) 35 | 36 | @property 37 | def list(self)-> list: 38 | """ return the python's list, to access more python list methods ;-) """ 39 | return self._ll 40 | 41 | def __add__(self,t:str): 42 | return StrClass(str(self) + t) 43 | def __radd__(self,t:str): 44 | return StrClass(t + str(self)) 45 | 46 | def __eq__(self,x): 47 | return str(self)==str(x) 48 | 49 | def __len__(self): 50 | return len(self._ll) 51 | 52 | def __str__(self): 53 | return " ".join(self._ll) 54 | def __repr__(self): 55 | return " ".join(self._ll) 56 | 57 | #--------------------------------------------------------------- 58 | 59 | keyize = lambda x: x.strip().lower() 60 | 61 | 62 | class DictStyle(dict): 63 | def __init__(self,parent): 64 | self._parent = parent 65 | dict.__init__(self,self._parent._ll) 66 | 67 | def __setitem__(self, k, v) -> None: 68 | self._parent.set(k,v,True) 69 | 70 | def clear(self) -> None: 71 | self._parent._ll=[] 72 | 73 | def update(self, d:dict): 74 | for k,v in d.items(): 75 | self[k]=v 76 | 77 | class StrStyle: 78 | """ expose basic methods set/get/contains/remove """ 79 | def __init__(self,txt=None): 80 | self._ll=[] 81 | if txt is not None: 82 | if isinstance(txt,dict): 83 | self._ll = list(txt.items()) 84 | else: 85 | for i in str(txt).split(";"): 86 | if i and ":" in i: 87 | k,v=i.strip().split(":",1) 88 | self._ll.append( (keyize(k),v.strip()) ) 89 | @property 90 | def dict(self): 91 | """ return a python's dict, to access more python dict methods ;-) """ 92 | return DictStyle(self) 93 | 94 | def set(self,k,v,unique=False): 95 | if unique: self.remove(k) 96 | self._ll.append( (keyize(k),v.strip()) ) 97 | 98 | def get(self,kk) -> list: 99 | return [v for k,v in self._ll if keyize(kk)==k] 100 | 101 | def contains(self,kk) -> int: 102 | return len(self.get(kk)) 103 | 104 | def __contains__(self,kk): 105 | return self.contains(kk)>0 106 | 107 | def remove(self,kk) -> bool: 108 | ll=[(k,v) for k,v in self._ll if keyize(kk)==k] 109 | for i in ll: 110 | self._ll.remove(i) 111 | return len(self._ll)>0 112 | 113 | def __add__(self,t:str): 114 | assert ":" in t 115 | return StrStyle(str(self) + t) 116 | def __radd__(self, t:str): 117 | assert ":" in t 118 | return StrStyle(t + str(self)) 119 | 120 | def __eq__(self,x): 121 | return str(self)==str(x) 122 | 123 | def __len__(self): 124 | return len(self._ll) 125 | 126 | def __str__(self): 127 | return "".join(["%s:%s;" %(k,v) for k,v in self._ll]) 128 | def __repr__(self): 129 | return "".join(["%s:%s;" %(k,v) for k,v in self._ll]) 130 | 131 | -------------------------------------------------------------------------------- /htag/runners/chromeappmode.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # # ############################################################################# 3 | # Copyright (C) 2022 manatlan manatlan[at]gmail(dot)com 4 | # 5 | # MIT licence 6 | # 7 | # https://github.com/manatlan/htag 8 | # ############################################################################# 9 | 10 | 11 | from .. import Tag 12 | from ..render import HRenderer 13 | from . import commons 14 | 15 | import os 16 | 17 | #="="="="="="="="="="="="="="="="="="="="="="="="="="="="="="="="="="="="="="=" 18 | # mainly code from the good old guy ;-) 19 | #="="="="="="="="="="="="="="="="="="="="="="="="="="="="="="="="="="="="="="=" 20 | import sys 21 | import shutil 22 | import tempfile 23 | import subprocess 24 | import logging 25 | import webbrowser 26 | 27 | logger = logging.getLogger(__name__) 28 | 29 | class FULLSCREEN: pass 30 | CHROMECACHE=".cache" 31 | 32 | class ChromeApp: 33 | def __init__(self, url, appname="driver",size=None,lockPort=None,chromeargs=[]): 34 | self._p=None 35 | 36 | def find_chrome_win(): 37 | import winreg # TODO: pip3 install winreg 38 | 39 | reg_path = r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe" 40 | for install_type in winreg.HKEY_CURRENT_USER, winreg.HKEY_LOCAL_MACHINE: 41 | try: 42 | with winreg.OpenKey(install_type, reg_path, 0, winreg.KEY_READ) as reg_key: 43 | return winreg.QueryValue(reg_key, None) 44 | except WindowsError: 45 | pass 46 | 47 | def find_chrome_mac(): 48 | default_dir = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" 49 | if os.path.exists(default_dir): 50 | return default_dir 51 | 52 | 53 | if sys.platform[:3] == "win": 54 | exe = find_chrome_win() 55 | elif sys.platform == "darwin": 56 | exe = find_chrome_mac() 57 | else: 58 | for i in ["chromium-browser", "chromium", "google-chrome", "chrome"]: 59 | try: 60 | exe = webbrowser.get(i).name 61 | break 62 | except webbrowser.Error: 63 | exe = None 64 | 65 | if not exe: 66 | raise Exception("no chrome browser, no app-mode !") 67 | else: 68 | args = [ #https://peter.sh/experiments/chromium-command-line-switches/ 69 | exe, 70 | "--app=" + url, # need to be a real http page ! 71 | "--app-id=%s" % (appname), 72 | "--app-auto-launched", 73 | "--no-first-run", 74 | "--no-default-browser-check", 75 | "--disable-notifications", 76 | "--disable-features=TranslateUI", 77 | "--autoplay-policy=no-user-gesture-required", 78 | #~ "--no-proxy-server", 79 | ] + chromeargs 80 | if size: 81 | if size == FULLSCREEN: 82 | args.append("--start-fullscreen") 83 | else: 84 | args.append( "--window-size=%s,%s" % (size[0],size[1]) ) 85 | 86 | if lockPort: #enable reusable cache folder (coz only one instance can be runned) 87 | self.cacheFolderToRemove=None 88 | args.append("--remote-debugging-port=%s" % lockPort) 89 | args.append("--disk-cache-dir=%s" % CHROMECACHE) 90 | args.append("--user-data-dir=%s/%s" % (CHROMECACHE,appname)) 91 | else: 92 | self.cacheFolderToRemove=os.path.join(tempfile.gettempdir(),appname+"_"+str(os.getpid())) 93 | args.append("--user-data-dir=" + self.cacheFolderToRemove) 94 | args.append("--aggressive-cache-discard") 95 | args.append("--disable-cache") 96 | args.append("--disable-application-cache") 97 | args.append("--disable-offline-load-stale-cache") 98 | args.append("--disk-cache-size=0") 99 | 100 | logger.debug("CHROME APP-MODE: %s"," ".join(args)) 101 | # self._p = subprocess.Popen(args) 102 | self._p = subprocess.Popen(args,stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) 103 | 104 | #~ if lockPort: 105 | #~ http_client = tornado.httpclient.HTTPClient() 106 | #~ self._ws = None 107 | #~ while self._ws == None: 108 | #~ try: 109 | #~ url = http_client.fetch("http://localhost:%s/json" % debugport).body 110 | #~ self._ws = json.loads(url)[0]["webSocketDebuggerUrl"] 111 | #~ except Exception as e: 112 | #~ self._ws = None 113 | 114 | def wait(self,thread): 115 | if self._p: 116 | self._p.wait() 117 | 118 | def __del__(self): # really important ! 119 | if self._p: 120 | self._p.kill() 121 | if self.cacheFolderToRemove: shutil.rmtree(self.cacheFolderToRemove, ignore_errors=True) 122 | 123 | #~ def _com(self, payload: dict): 124 | #~ """ https://chromedevtools.github.io/devtools-protocol/tot/Browser/#method-close """ 125 | #~ payload["id"] = 1 126 | #~ r=json.loads(wsquery(self._ws,json.dumps(payload)))["result"] 127 | #~ return r or True 128 | 129 | #~ def focus(self): # not used 130 | #~ return self._com(dict(method="Page.bringToFront")) 131 | 132 | #~ def navigate(self, url): # not used 133 | #~ return self._com(dict(method="Page.navigate", params={"url": url})) 134 | 135 | def exit(self): 136 | #~ self._com(dict(method="Browser.close")) 137 | if self._p: 138 | self._p.kill() 139 | #="="="="="="="="="="="="="="="="="="="="="="="="="="="="="="="="="="="="="="=" 140 | -------------------------------------------------------------------------------- /htag/runners/commons/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # # ############################################################################# 3 | # Copyright (C) 2022 manatlan manatlan[at]gmail(dot)com 4 | # 5 | # MIT licence 6 | # 7 | # https://github.com/manatlan/htag 8 | # ############################################################################# 9 | 10 | import json 11 | import urllib.parse 12 | 13 | def url2ak(url:str): 14 | """ transform the querystring of 'url' to (*args,**kargs)""" 15 | info = urllib.parse.urlsplit(url) 16 | args=[] 17 | kargs={} 18 | if info.query: 19 | items=info.query.split("&") 20 | first=lambda x: x[0] 21 | for i in items: 22 | if i: 23 | if "=" in i: 24 | if i.endswith("="): 25 | kargs[ i.split("=")[0] ] = None 26 | else: 27 | tt=list(urllib.parse.parse_qs(i).items())[0] 28 | kargs[ tt[0] ] = first(tt[1]) 29 | else: 30 | tt=list(urllib.parse.parse_qs("x="+i).items())[0] 31 | args.append( first(tt[1]) ) 32 | return tuple(args), dict(kargs) 33 | 34 | #---------------------------------------------- 35 | import re 36 | def match(mpath,path): 37 | "return a dict of declared vars from mpath if found in path" 38 | mode={ 39 | "str": r"[^/]+", # default 40 | "int": r"\\d+", 41 | "path": r".+", 42 | } 43 | 44 | #TODO: float, uuid ... like https://www.starlette.io/routing/#path-parameters 45 | 46 | patterns=[ 47 | (re.sub( r"{(\w[\w\d_]+)}" , r"(?P<\1>%s)" % mode["str"], mpath), lambda x: x), 48 | (re.sub( r"{(\w[\w\d_]+):str}" , r"(?P<\1>%s)" % mode["str"], mpath), lambda x: x), 49 | (re.sub( r"{(\w[\w\d_]+):int}" , r"(?P<\1>%s)" % mode["int"], mpath), lambda x: int(x)), 50 | (re.sub( r"{(\w[\w\d_]+):path}" , r"(?P<\1>%s)" % mode["path"], mpath), lambda x: x), 51 | ] 52 | 53 | dico={} 54 | for pattern,cast in patterns: 55 | g=re.match(pattern,path) 56 | if g: 57 | dico.update( {k:cast(v) for k,v in g.groupdict().items()} ) 58 | return dico 59 | 60 | #---------------------------------------------- 61 | import json,os 62 | class SessionFile(dict): 63 | def __init__(self,file): 64 | self._file=file 65 | 66 | if os.path.isfile(self._file): 67 | with open(self._file,"r+") as fid: 68 | d=json.load(fid) 69 | else: 70 | d={} 71 | 72 | super().__init__( d ) 73 | 74 | def __delitem__(self,k:str): 75 | super().__delitem__(k) 76 | self._save() 77 | 78 | def __setitem__(self,k:str,v): 79 | super().__setitem__(k,v) 80 | self._save() 81 | 82 | def clear(self): 83 | super().clear() 84 | self._save() 85 | 86 | def _save(self): 87 | if len(self): 88 | with open(self._file,"w+") as fid: 89 | json.dump(dict(self),fid, indent=4) 90 | else: 91 | if os.path.isfile(self._file): 92 | os.unlink(self._file) -------------------------------------------------------------------------------- /htag/runners/pyscript.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # # ############################################################################# 3 | # Copyright (C) 2022 manatlan manatlan[at]gmail(dot)com 4 | # 5 | # MIT licence 6 | # 7 | # https://github.com/manatlan/htag 8 | # ############################################################################# 9 | 10 | from .. import Tag 11 | from ..render import HRenderer 12 | from . import commons 13 | 14 | import json 15 | 16 | class PyScript: 17 | 18 | def __init__(self,tagClass:type): 19 | #TODO: __init__ could accept a 'file' parameter based on localstorage to make persistent session ! 20 | assert issubclass(tagClass,Tag) 21 | self.tagClass=tagClass 22 | 23 | def run(self,window=None): # **DEPRECATED** window: "pyscript js.window" 24 | if window is None: 25 | try: 26 | from js import window # this import should work in pyscript context ;-) 27 | except: 28 | pass 29 | self.window=window 30 | 31 | js = """ 32 | interact=async function(o) { 33 | action( await window.interactions( JSON.stringify(o) ) ); 34 | } 35 | 36 | function pyscript_starter() { 37 | if(document.querySelector("*[needToLoadBeforeStart]")) 38 | window.setTimeout( pyscript_starter, 100 ) 39 | else 40 | window.start() 41 | } 42 | """ 43 | self.hr = HRenderer(self.tagClass, js, init=commons.url2ak( window.document.location.href )) 44 | self.hr.sendactions=self.updateactions 45 | 46 | window.interactions = self.interactions 47 | assert window.document.head, "No in " 48 | assert window.document.body, "No in " 49 | 50 | # install statics in headers 51 | window.document.head.innerHTML="" 52 | for s in self.hr._statics: 53 | if isinstance(s,Tag): 54 | tag=window.document.createElement(s.tag) 55 | tag.innerHTML = "".join([str(i) for i in s.childs if i is not None]) 56 | for key,value in s.attrs.items(): 57 | setattr(tag, key, value) 58 | if key in ["src","href"]: 59 | tag.setAttribute("needToLoadBeforeStart", True) 60 | tag.onload = lambda o: o.target.removeAttribute("needToLoadBeforeStart") 61 | 62 | window.document.head.appendChild(tag) 63 | 64 | # install the first object in body 65 | window.document.body.outerHTML=str(self.hr) 66 | 67 | # and start the process 68 | window.pyscript_starter() # will run window.start, when dom ready 69 | 70 | async def interactions(self, o): 71 | data=json.loads(o) 72 | actions = await self.hr.interact( data["id"], data["method"], data["args"], data["kargs"], data.get("event") ) 73 | return json.dumps(actions) 74 | 75 | async def updateactions(self, actions:dict): 76 | self.window.action( json.dumps(actions) ) # send action as json (not a js(py) object) ;-( 77 | return True 78 | -------------------------------------------------------------------------------- /htag/runners/pywebview.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # # ############################################################################# 3 | # Copyright (C) 2022 manatlan manatlan[at]gmail(dot)com 4 | # 5 | # MIT licence 6 | # 7 | # https://github.com/manatlan/htag 8 | # ############################################################################# 9 | 10 | from .. import Tag 11 | from ..render import HRenderer 12 | from . import commons 13 | 14 | 15 | import asyncio,os 16 | import webview 17 | 18 | 19 | """ 20 | LIMITATION : 21 | 22 | til pywebview doen't support async jsapi ...(https://github.com/r0x0r/pywebview/issues/867) 23 | it can't work for "async generator" with the asyncio.run() trick (line 50) 24 | 25 | pywebview doesn't support : 26 | - document location changes 27 | - handling query_params from url 28 | - "tag.update()" 29 | """ 30 | 31 | class PyWebView: 32 | """ Open the rendering in a pywebview instance 33 | Interactions with builtin pywebview.api ;-) 34 | """ 35 | def __init__(self,tagClass:Tag,file:"str|None"=None): 36 | self._hr_session=commons.SessionFile(file) if file else None 37 | assert issubclass(tagClass,Tag) 38 | 39 | js = """ 40 | async function interact( o ) { 41 | action( await pywebview.api.interact( o["id"], o["method"], o["args"], o["kargs"], o["event"] ) ); 42 | } 43 | 44 | window.addEventListener('pywebviewready', start ); 45 | """ 46 | 47 | self.renderer=HRenderer(tagClass, js, lambda: os._exit(0), session=self._hr_session) 48 | 49 | def run(self): 50 | class Api: 51 | def interact(this,tagid,method,args,kargs,event): 52 | return asyncio.run(self.renderer.interact(tagid,method,args,kargs,event)) 53 | 54 | window = webview.create_window(self.renderer.title, html=str(self.renderer), js_api=Api(),text_select=True) 55 | webview.start(debug=False) 56 | 57 | def exit(self,rc=0): 58 | os._exit(rc) 59 | -------------------------------------------------------------------------------- /htag/ui.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # # ############################################################################# 3 | # Copyright (C) 2024 manatlan manatlan[at]gmail(dot)com 4 | # 5 | # MIT licence 6 | # 7 | # https://github.com/manatlan/htag 8 | # ############################################################################# 9 | try: 10 | from htagui.basics import * # htagui>=0.3 11 | except ImportError: 12 | from htagui import * # htagui<0.3 13 | -------------------------------------------------------------------------------- /manual_tests_base.py: -------------------------------------------------------------------------------- 1 | #!./venv/bin/python3 2 | from htag import Tag,Runner 3 | 4 | class App(Tag.body): 5 | def init(self): 6 | self.ph=Tag.div() #placeholder 7 | 8 | self <= Tag.button("nimp", _onclick=self.print) 9 | self <= Tag.input( _onkeyup=self.print) 10 | self <= self.ph 11 | 12 | def print(self,o): 13 | self.ph.clear(str(o.event)) 14 | 15 | 16 | if __name__ == "__main__": 17 | app=Runner( App ) 18 | 19 | import logging 20 | logging.basicConfig(format='[%(levelname)-5s] %(name)s: %(message)s',level=logging.INFO) 21 | logging.getLogger("htag.tag").setLevel( logging.INFO ) 22 | app.run() 23 | -------------------------------------------------------------------------------- /manual_tests_events.py: -------------------------------------------------------------------------------- 1 | #!./venv/bin/python3 2 | from htag import Tag # the only thing you'll need ;-) 3 | 4 | def nimp(obj): 5 | print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") 6 | 7 | 8 | class MyTag(Tag.span): 9 | def __init__(self,titre,callback): 10 | self.titre = titre 11 | super().__init__( titre + Tag.Button("x",_class="delete",_onclick=callback), _class="tag",_style="margin:4px" ) 12 | 13 | 14 | class Page(Tag.body): 15 | 16 | def init(self): 17 | 18 | def ff(obj): 19 | self <= "ff" 20 | 21 | def ffw(obj,size): 22 | self <= "f%s" %size 23 | 24 | def aeff(obj): 25 | print(obj) 26 | obj <= "b" 27 | 28 | 29 | # EVEN NEW MECHANISM 30 | self <= Tag.button( "TOP", _onclick=self.bind(ffw,b"window.innerWidth") ) 31 | self <= Tag.button( "TOP2", _onclick=self.bind(ffw,"toto") ) 32 | self <= MyTag( "test", aeff ) 33 | self <= Tag.button( "Stream In", _onclick=lambda o: self.stream() ) # stream in current button ! 34 | self <= Tag.button( "Stream Out", _onclick=self.bind( self.stream )) # stream in parent obj 35 | self <= Tag.button( "Stream Out", _onclick=self.bind.stream() ) 36 | 37 | self <= "
" 38 | 39 | # NEW MECHANISM 40 | self <= Tag.button( "externe (look in console)", _onclick=nimp ) 41 | self <= Tag.button( "lambda", _onclick=lambda o: ffw(o,"lambda") ) 42 | #/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\ 43 | #/\ try something new (multiple callbacks as a list) 44 | #/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\ 45 | self <= Tag.button( "**NEW**", _onclick=[ff,self.mm,lambda o: ffw(o,"KIKI")] ) 46 | #/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\ 47 | self <= Tag.button( "ff", _onclick=ff ) 48 | self <= Tag.button( "mm", _onclick=self.mm ) 49 | 50 | self <= Tag.button( "ymm", _onclick=self.ymm ) 51 | self <= Tag.button( "amm", _onclick=self.amm ) 52 | self <= Tag.button( "aymm", _onclick=self.aymm ) 53 | self <= "
" 54 | 55 | # OLD MECHANISM 56 | self <= Tag.button( "mm", _onclick=self.bind.mm("x") ) 57 | self <= Tag.button( "ymm", _onclick=self.bind.ymm("x") ) 58 | self <= Tag.button( "amm", _onclick=self.bind.amm("x") ) 59 | self <= Tag.button( "aymm", _onclick=self.bind.aymm("x") ) 60 | self <= "
" 61 | 62 | def mm(self,obj): 63 | self<="mm" 64 | 65 | def ymm(self,obj): 66 | self<="mm1" 67 | yield 68 | self<="mm2" 69 | 70 | async def amm(self,obj): 71 | self <= "amm" 72 | 73 | async def aymm(self,obj): 74 | self <= "aymm1" 75 | yield 76 | self <= "aymm2" 77 | 78 | def stream(self): 79 | yield "a" 80 | yield "b" 81 | yield ["c","d"] 82 | yield MyTag("kiki", nimp) 83 | 84 | App=Page 85 | from htag.runners import DevApp as Runner 86 | app=Runner( Page ) 87 | if __name__ == "__main__": 88 | import logging 89 | logging.basicConfig(format='[%(levelname)-5s] %(name)s: %(message)s',level=logging.DEBUG) 90 | logging.getLogger("htag.tag").setLevel( logging.INFO ) 91 | app.run() -------------------------------------------------------------------------------- /manual_tests_expose.py: -------------------------------------------------------------------------------- 1 | #!./venv/bin/python3 2 | from htag import Tag,expose 3 | Tag.STRICT_MODE=True 4 | 5 | class App(Tag.div): 6 | def init(self): 7 | # the old bind (for js interact) 8 | self+=Tag.button("test1",_onclick=self.bind.python(3,"[1]")) 9 | 10 | # the new bind (for js interact) 11 | self+=Tag.button("test2",_onclick=self.bind(self.python,4,b"[2]")) 12 | 13 | # create a js caller (only old bind) 14 | #~ self.js = "self.python=function(_) {%s}" % self.bind.python(b"...arguments") 15 | #~ self+=Tag.button("test",_onclick='self.python(5,"x")') 16 | 17 | # ^^ REPLACE something like that ^^ 18 | self+=Tag.button("test3",_onclick='this.parentNode.python(6,"[3]")') 19 | 20 | @expose # only needed for button 'test3' 21 | def python(self,nb,data): 22 | self+= nb * str(data) 23 | 24 | from htag.runners import BrowserHTTP as Runner 25 | 26 | app=Runner( App ) 27 | if __name__=="__main__": 28 | app.run() 29 | 30 | -------------------------------------------------------------------------------- /manual_tests_htbulma.py: -------------------------------------------------------------------------------- 1 | #!./venv/bin/python3 2 | from htag import Tag 3 | import htbulma as b 4 | 5 | class Star1(Tag.div): # it's a component ;-) 6 | """ rendering lately (using render (called before __str__)) 7 | 8 | it's simpler ... but event/action shouldn't try to draw something, coz render will rebuild all at each time 9 | """ 10 | 11 | def init(self,value=0): 12 | self.value=value 13 | 14 | def inc(self,v): 15 | self.value+=v 16 | 17 | def render(self): # <- ensure dynamic rendering 18 | self.clear() 19 | self <= b.Button( "-", _onclick = lambda o: self.inc(-1), _class="is-small" ) 20 | self <= b.Button( "+", _onclick = lambda o: self.inc(+1), _class="is-small" ) 21 | self <= "⭐"*self.value 22 | 23 | class Star2(Tag.div): # it's a component ;-) 24 | """ rendering immediatly (each event does the rendering) 25 | 26 | it's less simple ... but event/action should redraw something ! 27 | 28 | """ 29 | 30 | def init(self,value=0): 31 | self.value=value 32 | self <= b.Button( "-", _onclick = lambda o: self.inc(-1), _class="is-small" ) 33 | self <= b.Button( "+", _onclick = lambda o: self.inc(+1), _class="is-small" ) 34 | 35 | self.content = Tag.span( "⭐"*self.value ) 36 | self <= self.content 37 | 38 | def inc(self,v): 39 | self.value+=v 40 | self.content.clear( "⭐"*self.value ) 41 | 42 | 43 | 44 | class App(Tag.body): 45 | 46 | def init(self): 47 | self._s = b.Service(self) 48 | 49 | nav = b.Nav("My App") 50 | nav.addEntry("entry 1", lambda: self._s.alert( "You choose 1" ) ) 51 | nav.addEntry("entry 2", lambda: self._s.alert( "You choose 2" ) ) 52 | 53 | tab = b.Tabs() 54 | tab.addTab("Tab 1", Tag.b("Set star1s") + Star1(12) ) 55 | tab.addTab("Tab 2", Tag.b("Set star2s") + Star2(12) ) 56 | 57 | self <= nav + b.Section( tab ) 58 | 59 | 60 | if __name__=="__main__": 61 | # import logging 62 | # logging.basicConfig(format='[%(levelname)-5s] %(name)s: %(message)s',level=logging.DEBUG) 63 | 64 | from htag.runners import * 65 | BrowserStarletteWS( App ).run() 66 | -------------------------------------------------------------------------------- /manual_tests_new_InternalCall.py: -------------------------------------------------------------------------------- 1 | #!./venv/bin/python3 2 | from htag import Tag # the only thing you'll need ;-) 3 | 4 | 5 | 6 | class Page(Tag.body): 7 | def init(self): 8 | # self.call( self.bind.doit("heelo") ) 9 | self.call.doit("heelo") 10 | 11 | def doit(self,msg): 12 | self+=msg 13 | 14 | App=Page 15 | # and execute it in a pywebview instance 16 | from htag.runners import * 17 | # PyWebWiew( Page ).run() 18 | 19 | # here is another runner, in a simple browser (thru ajax calls) 20 | # ChromeApp( Page ).run() 21 | # BrowserHTTP( Page ).run() 22 | app=DevApp( Page ) 23 | if __name__ == "__main__": 24 | # BrowserTornadoHTTP( Page ).run() 25 | 26 | import logging 27 | logging.basicConfig(format='[%(levelname)-5s] %(name)s: %(message)s',level=logging.DEBUG) 28 | logging.getLogger("htag.tag").setLevel( logging.INFO ) 29 | 30 | 31 | app.run() 32 | -------------------------------------------------------------------------------- /manual_tests_persitent.py: -------------------------------------------------------------------------------- 1 | #!./venv/bin/python3 2 | from htag import Tag # the only thing you'll need ;-) 3 | 4 | class Nimp(Tag.div): 5 | def init(self): 6 | # !!! previous3 will not be saved in '/tmp/AEFFFF.json' !!! 7 | # (it's not the main/managed tag (which is Page), so it's an inner dict) 8 | self.state["previous3"]=self.state.get("previous3","") + "!" 9 | self.clear( self.state["previous3"] ) 10 | 11 | 12 | class Page(Tag.body): 13 | def init(self): 14 | self.session["previous"]=self.session.get("previous","") + "!" 15 | self.state["previous2"]=self.state.get("previous2","") + "!" 16 | 17 | self+=Tag.div( self.session["previous"] ) 18 | self+=Tag.div( self.state["previous2"] ) 19 | self+=Nimp() 20 | 21 | 22 | # from htag.runners import DevApp as Runner 23 | from htag.runners import ChromeApp as Runner 24 | # from htag.runners import WinApp as Runner 25 | # from htag.runners import BrowserTornadoHTTP as Runner 26 | # from htag.runners import BrowserStarletteWS as Runner 27 | # from htag.runners import BrowserStarletteWS as Runner 28 | # from htag.runners import BrowserHTTP as Runner 29 | # from htag.runners import AndroidApp as Runner 30 | # from htag.runners import PyWebView as Runner 31 | 32 | app=Runner( Page , file="/tmp/AEFFFF.json") 33 | if __name__ == "__main__": 34 | import logging 35 | logging.basicConfig(format='[%(levelname)-5s] %(name)s: %(message)s',level=logging.ERROR) 36 | logging.getLogger("htag.tag").setLevel( logging.ERROR ) 37 | # app.run() 38 | app.run() 39 | -------------------------------------------------------------------------------- /manual_tests_qp.py: -------------------------------------------------------------------------------- 1 | #!./venv/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from htag import Tag 5 | import asyncio,time 6 | 7 | class App(Tag.body): 8 | 9 | def init(self,param="nada"): 10 | self["style"]="background:#EEE;" 11 | self <= Tag.h3("param = "+param) 12 | self <= Tag.a("test '?' ?",_href="?",_style="display:block") 13 | self <= Tag.a("test '?param=A1'",_href="?param=A1",_style="display:block") 14 | self <= Tag.a("test '?param=A2'",_href="?param=A2",_style="display:block") 15 | self <= Tag.button("error", _onclick=lambda o: fdsgdfgfdsgfds()) 16 | self <= Tag.button("add content", _onclick=self.add_content) # just to control interact 17 | self <= Tag.button("EXIT app", _onclick=lambda o: self.exit()) # just to test QUIT/EXIT app 18 | self <= Tag.hr() 19 | 20 | self <= Tag.h3("Only if it handles tha '/other' route (DevApp/htagweb) :") 21 | self <= Tag.a("test '/other'" ,_href="/other",_style="display:block") 22 | self <= Tag.a("test '/other?pablo'",_href="/other?pablo",_style="display:block") 23 | self <= Tag.hr() 24 | 25 | self <= Tag.iframe(_src="/item/42") 26 | 27 | # self.place=Tag.div("this should be updated... no?") 28 | # self <= self.place 29 | # asyncio.ensure_future( self.loop_timer() ) 30 | 31 | async def loop_timer(self): 32 | while 1: 33 | await asyncio.sleep(0.5) 34 | self.place.clear(time.time() ) 35 | if not await self.place.update(): # update component (without interaction) 36 | # break if can't (<- good practice to kill this asyncio/loop) 37 | print("asyncio loop stopped") 38 | break 39 | 40 | 41 | def add_content(self,o): 42 | self <= "X " 43 | 44 | 45 | #================================================================================= 46 | #--------------------------------------------------------------------------------- 47 | # from htag.runners import DevApp as Runner # with .serve() and no QUIT 48 | 49 | # from htag.runners import BrowserHTTP as Runner 50 | # from htag.runners import BrowserStarletteWS as Runner 51 | # from htag.runners import BrowserStarletteHTTP as Runner 52 | # from htag.runners import BrowserTornadoHTTP as Runner 53 | # from htag.runners import ChromeApp as Runner 54 | # from htag.runners import AndroidApp as Runner 55 | # from htag.runners import PyWebView as Runner # just the "add content" will work (no query params / no other route) 56 | 57 | from htag.runners import Runner,HTTPResponse 58 | app=Runner(App,reload=False,debug=True,interface=(400,400),use_first_free_port=True) 59 | 60 | class AnotherApp(Tag.body): 61 | 62 | def init(self, name="vide"): 63 | self["style"]="background:#FFE;" 64 | self <= "Hello "+name 65 | self <= Tag.button("add content", _onclick=self.add_content) 66 | 67 | def add_content(self,o): 68 | self <= "X " 69 | 70 | #note : no path_params/query_params in route path ! 71 | app.add_route( "/other", lambda request: app.handle(request, AnotherApp ) ) 72 | 73 | 74 | async def handlerItem( request ): 75 | idx=request.path_params.get("idx") 76 | txt=request.query_params.get("txt") 77 | return HTTPResponse(200,"Numero %d (txt=%s)" % (idx,txt)) 78 | 79 | app.add_route( "/item/{idx:int}", handlerItem ) 80 | 81 | 82 | if __name__=="__main__": 83 | app.run() 84 | -------------------------------------------------------------------------------- /manual_tests_remove.py: -------------------------------------------------------------------------------- 1 | #!./venv/bin/python3 2 | # -*- coding: utf-8 -*- 3 | from htag import Tag # the only thing you'll need ;-) 4 | 5 | 6 | 7 | class Button(Tag.button): 8 | def init(self,txt): 9 | # we set some html attributs 10 | self["class"]="my" # set @class to "my" 11 | self["onclick"]=self.onclick # bind a js event on @onclick 12 | self<= txt 13 | 14 | def onclick(self): 15 | print("REMOVE",self.innerHTML) 16 | self.remove() 17 | 18 | 19 | class Page(Tag.body): # define a , but the renderer will force it to in all cases 20 | """ This is the main Tag, it will be rendered as by the htag/renderer """ 21 | statics = b"function error(m) {document.body.innerHTML += m}",".fun {background: red}" 22 | def init(self): 23 | 24 | self <= Button("A") 25 | self <= Button("A2") 26 | self <= Button("A3") 27 | self <= Button("A4") 28 | self <= Button("A5") 29 | self <= Tag.button("error", _onclick=lambda o: fdsgdfgfdsgfds()) 30 | 31 | self <= Tag.button("print",_onclick=self.print,_class="ki") 32 | 33 | def print(self,o): 34 | print("Should appear", o["class"]) 35 | o["class"].toggle("fun") 36 | 37 | class Page2(Tag.body): # define a , but the renderer will force it to in all cases 38 | def init(self): 39 | self+="Hello" 40 | self+=Tag.a("remover",_href="/p") 41 | 42 | 43 | App=Page 44 | 45 | # and execute it in a pywebview instance 46 | from htag.runners import * 47 | # PyWebWiew( Page ).run() 48 | 49 | # here is another runner, in a simple browser (thru ajax calls) 50 | # ChromeApp( Page ).run() 51 | # BrowserHTTP( Page ).run() 52 | app=DevApp( Page ) 53 | app.add_route("/p", lambda request: app.handle( request, Page ) ) 54 | app.add_route("/b", lambda request: app.handle( request, Page2 ) ) 55 | if __name__ == "__main__": 56 | # BrowserTornadoHTTP( Page ).run() 57 | app.run() 58 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: HTag's Docs 2 | site_description: GUI toolkit for building GUI toolikit (for building applications for desktop, web, and mobile from a single python3 codebase) 3 | 4 | theme: 5 | logo: htag.png 6 | name: 'material' 7 | palette: 8 | primary: 'black' 9 | accent: 'black' 10 | 11 | repo_name: manatlan/htag 12 | repo_url: https://github.com/manatlan/htag 13 | edit_uri: "" 14 | 15 | nav: 16 | - Introduction: 'index.md' 17 | - Tutorial: 'tutorial.md' 18 | - Concepts: concepts.md 19 | - Runners: 'runners.md' 20 | 21 | markdown_extensions: 22 | - markdown.extensions.codehilite: 23 | guess_lang: false 24 | - admonition 25 | - pymdownx.highlight: 26 | anchor_linenums: true 27 | - pymdownx.inlinehilite 28 | - pymdownx.snippets 29 | - pymdownx.superfences -------------------------------------------------------------------------------- /old_runners/README.md: -------------------------------------------------------------------------------- 1 | Theses are the good old runners (htag < 0.90.0). 2 | 3 | And are here, just for the posterity (there will not be maintained anymore) 4 | 5 | There were in "htag/runners" -------------------------------------------------------------------------------- /old_runners/browserhttp.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # # ############################################################################# 3 | # Copyright (C) 2022 manatlan manatlan[at]gmail(dot)com 4 | # 5 | # MIT licence 6 | # 7 | # https://github.com/manatlan/htag 8 | # ############################################################################# 9 | 10 | from .. import Tag 11 | from ..render import HRenderer 12 | from . import commons 13 | 14 | import asyncio 15 | import socket 16 | import webbrowser,os,json 17 | import urllib.parse 18 | 19 | class BrowserHTTP: 20 | """ Simple ASync Web Server with HTTP interactions with HTag. (only stdlib!) 21 | Open the rendering in a browser tab. 22 | 23 | Should not be used AS IS in a real app ... 24 | But it's the perfect runner, to test/debug, coz interactions are easier ! 25 | """ 26 | 27 | def __init__(self,tagClass:Tag,file:"str|None"=None): 28 | self._hr_session=commons.SessionFile(file) if file else None 29 | 30 | self.hrenderer=None 31 | self.tagClass=tagClass 32 | 33 | def instanciate(self,url:str): 34 | init = commons.url2ak(url) 35 | if self.hrenderer and self.hrenderer.init == init: 36 | return self.hrenderer 37 | 38 | js = """ 39 | async function interact( o ) { 40 | action( await (await window.fetch("/",{method:"POST", body:JSON.stringify(o)})).text() ) 41 | } 42 | 43 | window.addEventListener('DOMContentLoaded', start ); 44 | """ 45 | return HRenderer(self.tagClass, js, lambda: os._exit(0), init=init, session=self._hr_session) 46 | 47 | def run(self, host="127.0.0.1", port=8000, openBrowser=True): # localhost, by default !! 48 | 49 | """ 50 | ASyncio http server with stdlib ;-) 51 | Inspired from https://www.pythonsheets.com/notes/python-asyncio.html 52 | """ 53 | 54 | def make_header(type="text/html"): 55 | header = "HTTP/1.1 200 OK\r\n" 56 | header += f"Content-Type: {type}\r\n" 57 | header += "\r\n" 58 | return header 59 | 60 | async def handler(conn): 61 | 62 | CHUNK_LIMIT=256 # must be minimal 63 | req = b'' 64 | while True: 65 | chunk = await loop.sock_recv(conn, CHUNK_LIMIT) 66 | if chunk: 67 | req += chunk 68 | if len(chunk) < CHUNK_LIMIT: 69 | break 70 | else: 71 | break 72 | 73 | 74 | try: 75 | # if req.startswith(b"GET / HTTP"): 76 | if req.startswith(b"GET /"): 77 | url=req.decode()[4:].split(" HTTP")[0] 78 | 79 | info = urllib.parse.urlsplit(url) 80 | if info.path=="/": 81 | self.hrenderer = self.instanciate(url) 82 | resp = make_header() 83 | resp += str(self.hrenderer) 84 | else: 85 | resp = "HTTP/1.1 404 NOT FOUND\r\n" 86 | elif req.startswith(b"POST / HTTP"): 87 | _,content = req.split(b"\r\n\r\n") 88 | data = json.loads(content.decode()) 89 | dico = await self.hrenderer.interact(data["id"],data["method"],data["args"],data["kargs"],data.get("event") ) 90 | resp = make_header("application/json") 91 | resp += json.dumps(dico) 92 | else: 93 | resp = "HTTP/1.1 404 NOT FOUND\r\n" 94 | except Exception as e: 95 | print("SERVER ERROR:",e) 96 | resp = "HTTP/1.1 500 SERVER ERROR\r\n" 97 | 98 | await loop.sock_sendall(conn, resp.encode()) 99 | conn.close() 100 | 101 | async def server(sock, loop): 102 | while True: 103 | conn, addr = await loop.sock_accept(sock) 104 | loop.create_task(handler(conn)) 105 | 106 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: 107 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 108 | s.setblocking(False) 109 | s.bind((host, port)) 110 | s.listen(10) 111 | 112 | loop = asyncio.get_event_loop() 113 | try: 114 | if openBrowser: 115 | webbrowser.open_new_tab(f"http://{host}:{port}") 116 | loop.run_until_complete(server(s, loop)) 117 | except KeyboardInterrupt: 118 | pass 119 | finally: 120 | loop.close() 121 | -------------------------------------------------------------------------------- /old_runners/browserstarlettehttp.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # # ############################################################################# 3 | # Copyright (C) 2022 manatlan manatlan[at]gmail(dot)com 4 | # 5 | # MIT licence 6 | # 7 | # https://github.com/manatlan/htag 8 | # ############################################################################# 9 | 10 | from .. import Tag 11 | from ..render import HRenderer 12 | from . import commons 13 | 14 | import os 15 | 16 | from starlette.applications import Starlette 17 | from starlette.responses import HTMLResponse,JSONResponse 18 | from starlette.routing import Route 19 | 20 | 21 | class BrowserStarletteHTTP(Starlette): 22 | """ Simple ASync Web Server (with starlette) with HTTP interactions with htag. 23 | Open the rendering in a browser tab. 24 | 25 | The instance is an ASGI htag app 26 | """ 27 | def __init__(self,tagClass:Tag,file:"str|None"=None): 28 | self._hr_session=commons.SessionFile(file) if file else None 29 | assert issubclass(tagClass,Tag) 30 | 31 | self.hrenderer = None 32 | self.tagClass = tagClass 33 | 34 | Starlette.__init__(self,debug=True, routes=[ 35 | Route('/', self.GET, methods=["GET"]), 36 | Route('/', self.POST, methods=["POST"]), 37 | ]) 38 | 39 | def instanciate(self,url:str): 40 | init = commons.url2ak(url) 41 | if self.hrenderer and self.hrenderer.init == init: 42 | return self.hrenderer 43 | 44 | js = """ 45 | async function interact( o ) { 46 | action( await (await window.fetch("/",{method:"POST", body:JSON.stringify(o)})).text() ) 47 | } 48 | 49 | window.addEventListener('DOMContentLoaded', start ); 50 | """ 51 | 52 | return HRenderer(self.tagClass, js, lambda: os._exit(0), init=init,session=self._hr_session) 53 | 54 | async def GET(self,request) -> HTMLResponse: 55 | self.hrenderer = self.instanciate( str(request.url) ) 56 | return HTMLResponse( str(self.hrenderer) ) 57 | 58 | async def POST(self,request) -> JSONResponse: 59 | data = await request.json() 60 | dico = await self.hrenderer.interact(data["id"],data["method"],data["args"],data["kargs"],data.get("event")) 61 | return JSONResponse(dico) 62 | 63 | def run(self, host="127.0.0.1", port=8000, openBrowser=True): # localhost, by default !! 64 | import uvicorn,webbrowser 65 | if openBrowser: 66 | webbrowser.open_new_tab(f"http://{host}:{port}") 67 | 68 | uvicorn.run(self, host=host, port=port) 69 | 70 | -------------------------------------------------------------------------------- /old_runners/browserstarlettews.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # ############################################################################# 3 | # Copyright (C) 2022 manatlan manatlan[at]gmail(dot)com 4 | # 5 | # MIT licence 6 | # 7 | # https://github.com/manatlan/htag 8 | # ############################################################################# 9 | 10 | from .. import Tag 11 | from ..render import HRenderer 12 | from . import commons 13 | 14 | 15 | import os,json 16 | from starlette.applications import Starlette 17 | from starlette.responses import HTMLResponse 18 | from starlette.routing import Route,WebSocketRoute 19 | from starlette.endpoints import WebSocketEndpoint 20 | 21 | import logging 22 | logger = logging.getLogger(__name__) 23 | 24 | 25 | class BrowserStarletteWS(Starlette): 26 | """ Simple ASync Web Server (with starlette) with WebSocket interactions with HTag. 27 | Open the rendering in a browser tab. 28 | 29 | The instance is an ASGI htag app 30 | """ 31 | def __init__(self,tagClass:Tag,file:"str|None"=None): 32 | self._hr_session=commons.SessionFile(file) if file else None 33 | assert issubclass(tagClass,Tag) 34 | self.hrenderer = None 35 | self.tagClass = tagClass 36 | 37 | async def _sendactions(ws, actions:dict) -> bool: 38 | try: 39 | await ws.send_text( json.dumps(actions) ) 40 | return True 41 | except Exception as e: 42 | logger.error("Can't send to socket, error: %s",e) 43 | return False 44 | 45 | 46 | class WsInteract(WebSocketEndpoint): 47 | encoding = "json" 48 | 49 | #========================================================= 50 | async def on_connect(this, websocket): 51 | 52 | # accept cnx 53 | await websocket.accept() 54 | 55 | # declare hr.sendactions (async method) 56 | self.hrenderer.sendactions = lambda actions: _sendactions(websocket,actions) 57 | 58 | #========================================================= 59 | 60 | async def on_receive(this, websocket, data): 61 | actions = await self.hrenderer.interact(data["id"],data["method"],data["args"],data["kargs"],data.get("event")) 62 | await _sendactions( websocket, actions ) 63 | 64 | Starlette.__init__(self,debug=True, routes=[ 65 | Route('/', self.GET, methods=["GET"]), 66 | WebSocketRoute("/ws", WsInteract), 67 | ]) 68 | 69 | 70 | def instanciate(self,url:str): 71 | init = commons.url2ak(url) 72 | if self.hrenderer and self.hrenderer.init == init: 73 | return self.hrenderer 74 | 75 | js = """ 76 | async function interact( o ) { 77 | ws.send( JSON.stringify(o) ); 78 | } 79 | 80 | var ws = new WebSocket("ws://"+document.location.host+"/ws"); 81 | ws.onopen = start; 82 | ws.onmessage = function(e) { 83 | action( e.data ); 84 | }; 85 | """ 86 | return HRenderer(self.tagClass, js, lambda: os._exit(0), init=init,session=self._hr_session) 87 | 88 | async def GET(self,request): 89 | self.hrenderer=self.instanciate( str(request.url) ) 90 | return HTMLResponse( str(self.hrenderer) ) 91 | 92 | def run(self, host="127.0.0.1", port=8000, openBrowser=True): # localhost, by default !! 93 | import uvicorn,webbrowser 94 | if openBrowser: 95 | webbrowser.open_new_tab(f"http://{host}:{port}") 96 | 97 | uvicorn.run(self, host=host, port=port) -------------------------------------------------------------------------------- /old_runners/browsertornadohttp.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # # ############################################################################# 3 | # Copyright (C) 2022 manatlan manatlan[at]gmail(dot)com 4 | # 5 | # MIT licence 6 | # 7 | # https://github.com/manatlan/htag 8 | # ############################################################################# 9 | 10 | from .. import Tag 11 | from ..render import HRenderer 12 | from . import commons 13 | 14 | import os,json,sys,asyncio 15 | 16 | import tornado.ioloop 17 | import tornado.web 18 | 19 | 20 | class BrowserTornadoHTTP: 21 | """ Simple ASync Web Server (with TORNADO) with HTTP interactions with htag. 22 | Open the rendering in a browser tab. 23 | """ 24 | def __init__(self,tagClass:Tag,file:"str|None"=None): 25 | self._hr_session=commons.SessionFile(file) if file else None 26 | assert issubclass(tagClass,Tag) 27 | 28 | self.hrenderer=None 29 | self.tagClass=tagClass 30 | 31 | self._routes=[] 32 | 33 | try: # https://bugs.python.org/issue37373 FIX: tornado/py3.8 on windows 34 | if sys.platform == 'win32': 35 | asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) 36 | except: 37 | pass 38 | 39 | def instanciate(self,url:str): 40 | init = commons.url2ak(url) 41 | if self.hrenderer and self.hrenderer.init == init: 42 | return self.hrenderer 43 | 44 | js = """ 45 | async function interact( o ) { 46 | action( await (await window.fetch("/",{method:"POST", body:JSON.stringify(o)})).text() ) 47 | } 48 | 49 | window.addEventListener('DOMContentLoaded', start ); 50 | """ 51 | return HRenderer(self.tagClass, js, lambda: os._exit(0), init=init,session=self._hr_session) 52 | 53 | 54 | def run(self, host="127.0.0.1", port=8000 , openBrowser=True): # localhost, by default !! 55 | 56 | class MainHandler(tornado.web.RequestHandler): 57 | async def get(this): 58 | self.hrenderer = self.instanciate( str(this.request.uri) ) 59 | this.write( str(self.hrenderer) ) 60 | async def post(this): 61 | data = json.loads( this.request.body.decode() ) 62 | dico = await self.hrenderer.interact(data["id"],data["method"],data["args"],data["kargs"],data.get("event")) 63 | this.write(json.dumps(dico)) 64 | 65 | if openBrowser: 66 | import webbrowser 67 | webbrowser.open_new_tab(f"http://{host}:{port}") 68 | 69 | handlers=[(r"/", MainHandler),] 70 | for path,handler in self._routes: 71 | handlers.append( ( path,handler ) ) 72 | app = tornado.web.Application( handlers ) 73 | app.listen(port) 74 | tornado.ioloop.IOLoop.current().start() 75 | 76 | def add_handler(self, path:str, handler:tornado.web.RequestHandler): 77 | self._routes.append( (path,handler) ) 78 | -------------------------------------------------------------------------------- /old_runners/winapp.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # ############################################################################# 3 | # Copyright (C) 2022 manatlan manatlan[at]gmail(dot)com 4 | # 5 | # MIT licence 6 | # 7 | # https://github.com/manatlan/htag 8 | # ############################################################################# 9 | 10 | from .. import Tag 11 | from ..render import HRenderer 12 | from . import commons 13 | from .chromeapp import _ChromeApp 14 | 15 | import traceback,sys 16 | import os,json,asyncio,html 17 | import tornado.ioloop 18 | import tornado.web 19 | import tornado.websocket 20 | 21 | 22 | import logging 23 | logger = logging.getLogger(__name__) 24 | 25 | class WinApp: 26 | 27 | """ This a "Chrome App Runner" : it runs the front in a "Chrome App mode" by re-using 28 | the current chrome installation, in a headless mode. 29 | 30 | It's the same as ChromeApp (which it reuses) ... BUT : 31 | - the backend use HTTP/WS with tornado (not uvicorn !!!!) 32 | - as it doesn't use uvicon, it's the perfect solution on windows (for .pyw files) 33 | """ 34 | 35 | def __init__(self,tagClass:Tag,file:"str|None"=None): 36 | self._hr_session=commons.SessionFile(file) if file else None 37 | assert issubclass(tagClass,Tag) 38 | self.tagClass = tagClass 39 | self.hrenderer = None 40 | 41 | self._routes=[] 42 | 43 | try: # https://bugs.python.org/issue37373 FIX: tornado/py3.8 on windows 44 | if sys.platform == 'win32': 45 | asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) 46 | except: 47 | pass 48 | 49 | def run(self, host="127.0.0.1", port=8000 , size=(800,600)): # localhost, by default !! 50 | 51 | def instanciate(url:str): 52 | init = commons.url2ak(url) 53 | if self.hrenderer and self.hrenderer.init == init: 54 | return self.hrenderer 55 | 56 | # ws.onerror and ws.onclose shouldn't be visible, because server side stops all when socket close ! 57 | js = """ 58 | 59 | async function interact( o ) { 60 | ws.send( JSON.stringify(o) ); 61 | } 62 | 63 | var ws = new WebSocket("ws://"+document.location.host+"/ws"); 64 | ws.onopen = start; 65 | ws.onmessage = function(e) { 66 | action( e.data ); 67 | }; 68 | ws.onerror = function(e) { 69 | console.error("WS ERROR"); 70 | }; 71 | ws.onclose = function(e) { 72 | window.close(); 73 | }; 74 | """ 75 | return HRenderer(self.tagClass, js, lambda: os._exit(0), init=init, fullerror=False, session=self._hr_session) 76 | 77 | class MainHandler(tornado.web.RequestHandler): 78 | async def get(this): 79 | self.hrenderer = instanciate( str(this.request.uri) ) 80 | this.write( str(self.hrenderer) ) 81 | 82 | async def _sendactions(ws, actions:dict) -> bool: 83 | try: 84 | await ws.write_message( json.dumps(actions) ) 85 | return True 86 | except Exception as e: 87 | logger.error("Can't send to socket, error: %s",e) 88 | return False 89 | 90 | class SocketHandler(tornado.websocket.WebSocketHandler): 91 | def open(this): 92 | # declare hr.sendactions (async method) 93 | self.hrenderer.sendactions = lambda actions: _sendactions(this,actions) 94 | 95 | async def on_message(this, data): 96 | data=json.loads(data) 97 | actions = await self.hrenderer.interact(data["id"],data["method"],data["args"],data["kargs"],data.get("event")) 98 | await _sendactions( this, actions ) 99 | 100 | def on_close(this): 101 | self.chromeapp.exit() 102 | os._exit(0) 103 | 104 | try: 105 | self.chromeapp = _ChromeApp(f"http://{host}:{port}",size=size) 106 | except: 107 | import webbrowser 108 | webbrowser.open_new_tab(f"http://{host}:{port}") 109 | class FakeChromeApp: 110 | def wait(self,thread): 111 | pass 112 | def exit(self): 113 | pass 114 | self.chromeapp = FakeChromeApp() 115 | handlers=[(r"/", MainHandler),(r"/ws", SocketHandler)] 116 | for path,handler in self._routes: 117 | handlers.append( ( path,handler ) ) 118 | app = tornado.web.Application(handlers) 119 | app.listen(port) 120 | tornado.ioloop.IOLoop.current().start() 121 | 122 | def add_handler(self, path:str, handler:tornado.web.RequestHandler): 123 | self._routes.append( (path,handler) ) 124 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "htag" 3 | version = "0.0.0" # auto-updated 4 | description = "GUI toolkit for building GUI toolkits (and create beautiful applications for mobile, web, and desktop from a single python3 codebase)" 5 | authors = ["manatlan "] 6 | readme = 'README.md' 7 | license="MIT" 8 | keywords=['gui', 'electron', "cef", "pywebview", "starlette", "uvicorn", "tornado", "asyncio", "desktop", "web", "mobile", "http", "websocket", "html", "pyscript", "android", "kivy", "apk"] 9 | homepage = "https://github.com/manatlan/htag" 10 | repository = "https://github.com/manatlan/htag" 11 | documentation = "https://manatlan.github.io/htag/" 12 | classifiers = [ 13 | "Operating System :: OS Independent", 14 | "Topic :: Software Development :: Libraries :: Python Modules", 15 | "Topic :: Software Development :: Build Tools", 16 | "License :: OSI Approved :: Apache Software License", 17 | ] 18 | 19 | [tool.poetry.dependencies] 20 | python = "^3.7" 21 | 22 | [tool.poetry.dev-dependencies] 23 | pytest = "^6" 24 | pytest-cov = "^3" 25 | pytest-asyncio="^0.19" 26 | pywebview = "^3.6" 27 | selenium = "^4" 28 | fake-winreg = "^1.6" 29 | 30 | [build-system] 31 | requires = ["poetry>=0.12"] 32 | build-backend = "poetry.masonry.api" -------------------------------------------------------------------------------- /selenium/app1.py: -------------------------------------------------------------------------------- 1 | #!./venv/bin/python3 2 | import sys,os; sys.path.insert(0,os.path.join( os.path.dirname(__file__),"..")) 3 | import hclient 4 | ################################################################################# 5 | # 6 | from htag import Tag 7 | 8 | class App(Tag.body): 9 | """ the base """ 10 | def init(self): 11 | def say_hello(o): 12 | self <= Tag.li("hello") 13 | self<= Tag.button("click",_onclick = say_hello) 14 | self<= Tag.button("exit",_onclick = lambda o: self.exit()) 15 | 16 | # 17 | ################################################################################# 18 | 19 | def tests(client:hclient.HClient): 20 | assert "App" in client.title 21 | 22 | client.click('//button[text()="click"]') 23 | client.click('//button[text()="click"]') 24 | client.click('//button[text()="click"]') 25 | 26 | assert len(client.find('//li'))==3 27 | 28 | client.click('//button[text()="exit"]') 29 | return True 30 | 31 | if __name__=="__main__": 32 | # hclient.normalRun(App) 33 | hclient.test( App, "WS", tests) 34 | # hclient.test( App, "HTTP", tests) 35 | # hclient.test( App, "PyScript", tests) #NEED a "poetry build" before !!!! 36 | -------------------------------------------------------------------------------- /selenium/app2.py: -------------------------------------------------------------------------------- 1 | #!./venv/bin/python3 2 | import sys,os; sys.path.insert(0,os.path.join( os.path.dirname(__file__),"..")) 3 | import hclient 4 | ################################################################################# 5 | # 6 | from htag import Tag 7 | 8 | class App(Tag.div): 9 | """ Yield UI """ 10 | 11 | imports=[] 12 | 13 | def init(self): 14 | self.call.drawui() 15 | 16 | def drawui(self): 17 | for i in range(3): 18 | yield 19 | self <= Tag.my_tag(f"content{i}") 20 | self.call.check( b"self.innerHTML" ) 21 | yield 22 | self.clear() 23 | self <= Tag.button("exit",_onclick = lambda o: self.exit()) 24 | 25 | def check(self,innerhtml): 26 | assert self.innerHTML == innerhtml 27 | 28 | # 29 | ################################################################################# 30 | 31 | def tests(client:hclient.HClient): 32 | assert "App" in client.title 33 | client.wait(2) 34 | client.click('//button[text()="exit"]') 35 | return True 36 | 37 | if __name__=="__main__": 38 | # hclient.normalRun(App) 39 | hclient.test( App, "WS", tests) 40 | # hclient.test( App, "HTTP", tests) 41 | # hclient.test( App, "PyScript", tests) #NEED a "poetry build" before !!!! 42 | -------------------------------------------------------------------------------- /selenium/app3.py: -------------------------------------------------------------------------------- 1 | #!./venv/bin/python3 2 | import sys,os; sys.path.insert(0,os.path.join( os.path.dirname(__file__),"..")) 3 | import hclient 4 | ################################################################################# 5 | # 6 | from htag import Tag 7 | 8 | class App(Tag.div): 9 | """ Stream UI """ 10 | 11 | imports=[] 12 | 13 | def init(self): 14 | self.call.streamui() 15 | 16 | def streamui(self): 17 | for i in range(3): 18 | yield Tag.my_tag(f"content{i}") 19 | self.call.check( b"self.innerHTML" ) 20 | yield 21 | self.clear() 22 | self <= Tag.button("exit",_onclick = lambda o: self.exit()) 23 | 24 | def check(self,innerhtml): 25 | assert self.innerHTML == innerhtml 26 | 27 | # 28 | # ################################################################################# 29 | 30 | def tests(client:hclient.HClient): 31 | assert "App" in client.title 32 | client.wait(2) 33 | client.click('//button[text()="exit"]') 34 | return True 35 | 36 | 37 | if __name__=="__main__": 38 | # hclient.normalRun(App) 39 | hclient.test( App, "WS", tests) 40 | # hclient.test( App, "HTTP", tests) 41 | # hclient.test( App, "PyScript", tests) #NEED a "poetry build" before !!!! 42 | -------------------------------------------------------------------------------- /selenium/app4.py: -------------------------------------------------------------------------------- 1 | #!./venv/bin/python3 2 | import sys,os; sys.path.insert(0,os.path.join( os.path.dirname(__file__),"..")) 3 | import hclient 4 | ################################################################################# 5 | # 6 | from htag import Tag 7 | 8 | STAR = "☆" 9 | 10 | class Stars(Tag.span): # it's a component ;-) 11 | def init(self,name,value=0): 12 | self.name=name 13 | self["class"]=name 14 | self["style"]="display:block" 15 | self.value=value 16 | self.bless = Tag.Button( self.name+"-", _onclick = lambda o: self.inc(-1) ) 17 | self.bmore = Tag.Button( self.name+"+", _onclick = lambda o: self.inc(+1) ) 18 | def inc(self,v): 19 | self.value+=v 20 | def render(self): 21 | self.clear() 22 | self += self.bless + self.bmore + (STAR*self.value) 23 | #------------------------------------------------------------ 24 | 25 | class App(Tag.div): # it's a component ;-) 26 | """ Using reactive component, in a reactive context 27 | """ 28 | 29 | def init(self): 30 | # here we use the previous component "Stars" 31 | # in a a reactive way (with a render method) 32 | self.s1= Stars("a") 33 | self.s2= Stars("b",2) 34 | self.s3= Stars("c",4) 35 | self.reset= Tag.Button( "Reset", _onclick = self.clickreset ) 36 | self.show = Tag.div(_class="show") 37 | self.exiter = Tag.button("exit",_onclick = lambda o: self.exit()) 38 | 39 | 40 | def render(self): # it's present -> it's used 41 | self.clear() 42 | 43 | # so the rendering is managed by htag 44 | self <= self.s1+self.s2+self.s3+self.reset + self.show + self.exiter 45 | 46 | # and so, this div will be updated at reset ! 47 | self.show.clear("Values: %s,%s,%s" % (self.s1.value,self.s2.value,self.s3.value)) 48 | 49 | def clickreset(self,o): 50 | # so, resetting values, will redraw this component (App) automatically 51 | self.s1.value=0 52 | self.s2.value=0 53 | self.s3.value=0 54 | 55 | # 56 | ################################################################################# 57 | 58 | def tests(client:hclient.HClient): 59 | assert "App" in client.title 60 | client.click('//button[text()="a+"]') 61 | client.click('//button[text()="a+"]') 62 | client.click('//button[text()="a+"]') 63 | client.click('//button[text()="a+"]') 64 | client.click('//button[text()="b+"]') 65 | client.click('//button[text()="b+"]') 66 | 67 | values=client.find("//div[@class='show']")[0] 68 | assert values.text == "Values: 4,4,4" 69 | assert client.find("//span[@class='a']")[0].text.count("☆")==4 70 | assert client.find("//span[@class='b']")[0].text.count("☆")==4 71 | assert client.find("//span[@class='c']")[0].text.count("☆")==4 72 | 73 | client.click('//button[text()="Reset"]') 74 | 75 | values=client.find("//div[@class='show']")[0] 76 | assert values.text == "Values: 0,0,0" 77 | assert client.find("//span[@class='a']")[0].text.count("☆")==0 78 | assert client.find("//span[@class='b']")[0].text.count("☆")==0 79 | assert client.find("//span[@class='c']")[0].text.count("☆")==0 80 | 81 | client.click('//button[text()="exit"]') 82 | 83 | return True 84 | 85 | if __name__=="__main__": 86 | # hclient.normalRun(App) 87 | hclient.test( App, "WS", tests) 88 | # hclient.test( App, "HTTP", tests) 89 | # hclient.test( App, "PyScript", tests) #NEED a "poetry build" before !!!! 90 | -------------------------------------------------------------------------------- /selenium/hclient.py: -------------------------------------------------------------------------------- 1 | ################################################################################################## 2 | ## the common framework between the github action : .github/workflows/selenium.yaml and IRL 3 | ################################################################################################## 4 | 5 | import time,sys,os 6 | 7 | from selenium import webdriver 8 | from selenium.webdriver.common.keys import Keys 9 | from selenium.webdriver.common.by import By 10 | from multiprocessing import Process 11 | 12 | class HClient: 13 | def __init__(self,driver): 14 | self.driver=driver 15 | 16 | @property 17 | def title(self): 18 | return self.driver.title 19 | 20 | def click(self,xp:str): 21 | print("CLICK:",xp) 22 | try: 23 | self.driver.find_element(By.XPATH, xp).click() 24 | time.sleep(1) 25 | except Exception as e: 26 | print("***HClient ERROR***",e) 27 | 28 | def find(self,xp:str) -> list: 29 | print("FIND:",xp) 30 | return self.driver.find_elements(By.XPATH, xp) 31 | 32 | def wait(self,nbs): 33 | time.sleep(nbs) 34 | 35 | def run(App,runner:str,openBrowser=True,port=8000): 36 | assert runner in ("PyScript","WS","HTTP") 37 | print("App runned in",runner) 38 | 39 | if runner=="PyScript": 40 | """ 41 | This thing is complex to test/develop (need to py-env the wheel), you'll need to do : 42 | - poetry build # to produce dist/htag-0.0.0-py3-none-any.whl 43 | - python3 -m http.server 8001 44 | - chrome http://localhost:8001/manual_pyscript.html 45 | 46 | """ 47 | content = """ 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | Starting pyscript ;-) 56 | 57 | 68 | 69 | """ 70 | 71 | import inspect,re 72 | import http.server 73 | import socketserver 74 | import webbrowser 75 | 76 | src = re.search(r"#(.+)#", open(inspect.getsourcefile(App)).read(),re.DOTALL) 77 | if src: 78 | src=src.group(1).strip() 79 | else: 80 | print("This app is not pyscript'able (miss '#(.+)#')") 81 | sys.exit(-1) 82 | try: 83 | with open("index.html","w+") as fid: 84 | fid.write(content % src) 85 | 86 | Handler = http.server.SimpleHTTPRequestHandler 87 | try: 88 | with socketserver.TCPServer(("", port), Handler) as httpd: 89 | print("serving at port", port) 90 | if openBrowser: 91 | webbrowser.open_new_tab(f"http://localhost:{port}") 92 | httpd.serve_forever() 93 | except Exception as e: 94 | print("can't start httpd server",e) 95 | sys.exit(-1) 96 | finally: 97 | os.unlink("index.html") 98 | elif runner == "WS": 99 | from htag.runners import Runner 100 | Runner(App,port=port,interface = 1 if openBrowser else 0).run() 101 | elif runner == "HTTP": 102 | from htag.runners import Runner 103 | Runner(App,port=port,interface = 1 if openBrowser else 0,http_only=True).run() 104 | 105 | def test(App,runner:str, tests): 106 | """ for test on a local machine only """ 107 | Process(target=run, args=(App, runner, False)).start() 108 | with webdriver.Chrome() as driver: 109 | driver.get("http://127.0.0.1:8000/") 110 | x=testDriver(driver,tests) 111 | print("-->",x and "OK" or "KO") 112 | 113 | def testDriver(driver,tests): 114 | #=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- # pyscript specific 115 | time.sleep(1) 116 | hc=HClient(driver) 117 | while 1: 118 | bodys=hc.find("//body") 119 | if bodys and ("Starting" not in bodys[0].text): 120 | break 121 | time.sleep(1) 122 | print("Start") 123 | #=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 124 | return tests( hc ) 125 | 126 | def normalRun(App): 127 | """ just for test/dev in a normal context """ 128 | from htag.runners import Runner 129 | Runner(App,interface = 1,use_first_free_port=True).run() # run in all cases (find a free port automatically) -------------------------------------------------------------------------------- /selenium/run.py: -------------------------------------------------------------------------------- 1 | ################################################################################################## 2 | ## Run the runner 'sys.argv[1]' with the App 'sys.argv[2]' 3 | ## used by github action : .github/workflows/selenium.yaml 4 | ################################################################################################## 5 | 6 | import sys,importlib 7 | ####################################################### 8 | runner = sys.argv[1] 9 | App=importlib.import_module(sys.argv[2]).App 10 | port=int(sys.argv[3]) if len(sys.argv)>3 else 8000 11 | ####################################################### 12 | import hclient 13 | hclient.run( App, runner, openBrowser=False, port=port) 14 | -------------------------------------------------------------------------------- /selenium/tests.py: -------------------------------------------------------------------------------- 1 | ################################################################################################## 2 | ## Run the selenium test on port 'sys.argv[1]' with the App 'sys.argv[2]' 3 | ## used by github action : .github/workflows/selenium.yaml 4 | ################################################################################################## 5 | 6 | import sys,os,time 7 | from selenium import webdriver 8 | from webdriver_manager.chrome import ChromeDriverManager 9 | try: 10 | from webdriver_manager.core.utils import ChromeType 11 | except ImportError: 12 | from webdriver_manager.core.os_manager import ChromeType 13 | from selenium.webdriver.chrome.options import Options 14 | from selenium.webdriver.chrome.service import Service 15 | import hclient 16 | 17 | 18 | import importlib 19 | ####################################################### 20 | port = sys.argv[1] 21 | tests=importlib.import_module(sys.argv[2]).tests 22 | ####################################################### 23 | 24 | chrome_service = Service(ChromeDriverManager(chrome_type=ChromeType.CHROMIUM).install()) 25 | 26 | chrome_options = Options() 27 | options = [ 28 | "--headless", 29 | "--disable-gpu", 30 | "--window-size=1920,1200", 31 | "--ignore-certificate-errors", 32 | "--disable-extensions", 33 | "--no-sandbox", 34 | "--disable-dev-shm-usage" 35 | ] 36 | for option in options: 37 | chrome_options.add_argument(option) 38 | 39 | with webdriver.Chrome(service=chrome_service, options=chrome_options) as driver: 40 | driver.get('http://localhost:'+port) 41 | x=hclient.testDriver(driver,tests) 42 | 43 | if x: 44 | print("----> OK") 45 | sys.exit(0) 46 | else: 47 | print("----> KO") 48 | sys.exit(-1) 49 | -------------------------------------------------------------------------------- /test_constructors.py: -------------------------------------------------------------------------------- 1 | from htag import Tag 2 | import pytest 3 | 4 | 5 | ##################################################################################### 6 | # test base 7 | ##################################################################################### 8 | 9 | def test_basic_creation(): # auto init (property/html_attributes) in all cases 10 | # test base constructor 11 | t=Tag.div("hello",param1=12,_param2=13) 12 | assert t.param1 == 12 13 | assert t["param2"] == 13 14 | 15 | class MyDiv(Tag.div): 16 | pass 17 | 18 | def test_basic_inherited_creation(): # auto init (property/html_attributes) in all cases 19 | # test Tag simple inherits (no constructor) 20 | t=MyDiv("hello",param1=12,_param2=13) 21 | assert t.param1 == 12 22 | assert t["param2"] == 13 23 | 24 | 25 | 26 | 27 | 28 | 29 | ##################################################################################### 30 | # test Tag constructor with real/python __init__() method 31 | ##################################################################################### 32 | 33 | ############################################# 34 | ## with real __init__() constructor 35 | ############################################# 36 | 37 | class MyRDivOpen(Tag.div): # accept auto set property/html_attributes 38 | def __init__(self,content,**a): 39 | Tag.div.__init__(self,content,**a) 40 | 41 | class MyRDivClosed(Tag.div): # DONT't accept auto set property/html_attributes 42 | def __init__(self,content): 43 | Tag.div.__init__(self,content) 44 | 45 | 46 | def test_real_inherited_creation(): 47 | # test Tag inherited with __init__() constructor 48 | t=MyRDivOpen("hi",param1=12,_param2=13) 49 | assert t.param1 == 12 50 | assert t["param2"] == 13 51 | 52 | t=MyRDivOpen(content="hi",param1=12,_param2=13) 53 | assert t.param1 == 12 54 | assert t["param2"] == 13 55 | 56 | with pytest.raises(TypeError): 57 | MyRDivClosed("hi",param1=12) 58 | 59 | with pytest.raises(TypeError): 60 | MyRDivClosed("hi",_param2=13) 61 | 62 | 63 | 64 | 65 | 66 | ##################################################################################### 67 | # test Tag constructor with simplified init() method 68 | ##################################################################################### 69 | 70 | 71 | ############################################# 72 | ## with simplified init() constructor 73 | ############################################# 74 | 75 | class MyDivOpen(Tag.div): # accept auto set property/html_attributes 76 | def init(self,content,**a): # <- **a 77 | self <= content 78 | 79 | class MyDivClosed(Tag.div): # DONT't accept auto set property/html_attributes 80 | def init(self,content): # no **a ! 81 | self <= content 82 | 83 | def test_simplified_inherited_creation(): 84 | 85 | # test Tag inherited with init() constructor 86 | t=MyDivOpen("hi",param1=12,_param2=13) 87 | assert t.param1 == 12 88 | assert t["param2"] == 13 89 | 90 | t=MyDivOpen(content="hi",param1=12,_param2=13) 91 | assert t.param1 == 12 92 | assert t["param2"] == 13 93 | 94 | with pytest.raises(TypeError): 95 | MyDivClosed("hi",param1=12) 96 | 97 | with pytest.raises(TypeError): 98 | MyDivClosed("hi",_param2=13) 99 | 100 | -------------------------------------------------------------------------------- /test_dom.py: -------------------------------------------------------------------------------- 1 | from htag import Tag,HTagException 2 | import pytest 3 | 4 | def test_base(): 5 | t=Tag.div() 6 | assert t.parent == None 7 | assert t.root == t 8 | 9 | 10 | def test_adding_None(): 11 | parent = Tag.div() 12 | parent.add(None) # does nothing ! 13 | parent <= None # does nothing ! 14 | parent += None # does nothing ! 15 | parent += [None,None] # does nothing ! 16 | assert len(parent.childs)==0 17 | 18 | def test_try_to_override_important_props(): 19 | t = Tag.div() 20 | with pytest.raises( AttributeError ): 21 | t.parent=None 22 | 23 | with pytest.raises( AttributeError ): 24 | t.root=None 25 | 26 | with pytest.raises( AttributeError ): 27 | t.event=None 28 | 29 | 30 | def test_unparenting_remove(): 31 | parent = Tag.div() 32 | child = Tag.span() 33 | childchild = Tag.b() 34 | 35 | parent <= child <= childchild 36 | assert str(parent)=="
" 37 | assert child.root == childchild.root == parent.root == parent 38 | 39 | assert child.parent == parent 40 | assert childchild.parent == child 41 | assert parent.childs[0] == child 42 | 43 | child.remove() 44 | assert str(parent)=="
" 45 | 46 | assert len(parent.childs) == 0 47 | assert child.parent == None 48 | assert childchild.parent == child 49 | 50 | assert parent.root == parent 51 | assert child.root == child 52 | assert childchild.root == child 53 | 54 | def test_unparenting_clear(): 55 | parent = Tag.div() 56 | child = Tag.span() 57 | childchild = Tag.b() 58 | 59 | parent <= child <= childchild 60 | assert str(parent)=="
" 61 | assert child.root == childchild.root == parent.root == parent 62 | 63 | parent.clear() 64 | assert str(parent)=="
" 65 | 66 | assert len(parent.childs) == 0 67 | assert child.parent == None 68 | assert childchild.parent == child 69 | 70 | assert parent.root == parent 71 | assert child.root == child 72 | assert childchild.root == child 73 | 74 | 75 | def test_cant_add_many_times(): 76 | parent1 = Tag.div() 77 | parent2 = Tag.div() 78 | 79 | parent1.STRICT_MODE=True 80 | parent2.STRICT_MODE=True 81 | 82 | 83 | a_child = Tag.span() 84 | parent1 += a_child 85 | 86 | # can't be added to another one 87 | with pytest.raises(HTagException): 88 | parent2 += a_child 89 | 90 | assert a_child.parent == parent1 91 | 92 | # clear parent1 93 | parent1.clear() 94 | 95 | # so the child is no more in parent1 96 | # we can add it to parent2 97 | parent2 += a_child 98 | assert a_child.parent == parent2 99 | 100 | ##################################################################### 101 | ##################################################################### 102 | ##################################################################### 103 | def t0(): 104 | parent = Tag.div() 105 | a_child = Tag.span() 106 | 107 | parent.add( a_child, True) # force reparent 108 | parent.add( a_child, True) # force reparent 109 | 110 | def t00(): 111 | parent = Tag.div() 112 | a_child = Tag.span() 113 | 114 | parent.add( a_child, False) # don't force reparent (default) 115 | parent.add( a_child, False) # don't force reparent (default) 116 | 117 | def t1(): 118 | parent = Tag.div() 119 | a_child = Tag.span() 120 | 121 | parent += a_child 122 | parent += a_child # raise 123 | 124 | def t2(): 125 | parent = Tag.div() 126 | a_child = Tag.span() 127 | 128 | parent += [a_child,a_child] # raise 129 | 130 | def t3(): 131 | parent = Tag.div() 132 | a_child = Tag.span() 133 | 134 | parent += Tag.div(a_child) 135 | parent += Tag.div(a_child) # raise 136 | 137 | 138 | def t4(): 139 | parent = Tag.div() 140 | a_child = Tag.span() 141 | 142 | parent <= Tag.div() <= a_child 143 | parent <= Tag.div() <= a_child # raise 144 | 145 | def t5(): 146 | parent = Tag.div() 147 | a_child = Tag.span() 148 | 149 | parent.childs.append( a_child ) # since 'childs' is a tuple -> AttributeError 150 | parent.childs.append( a_child ) 151 | 152 | def test_strictmode_off(): 153 | old=Tag.STRICT_MODE 154 | try: 155 | Tag.STRICT_MODE=False 156 | 157 | t0() 158 | t00() 159 | 160 | t1() 161 | t2() 162 | t3() 163 | t4() 164 | 165 | with pytest.raises(AttributeError): # AttributeError: 'tuple' object has no attribute 'append' 166 | t5() 167 | finally: 168 | Tag.STRICT_MODE=old 169 | 170 | def test_strictmode_on(): 171 | old=Tag.STRICT_MODE 172 | try: 173 | Tag.STRICT_MODE=True 174 | 175 | t0() 176 | 177 | with pytest.raises(HTagException): 178 | t00() 179 | 180 | with pytest.raises(HTagException): 181 | t1() 182 | 183 | with pytest.raises(HTagException): 184 | t2() 185 | 186 | with pytest.raises(HTagException): 187 | t3() 188 | 189 | with pytest.raises(HTagException): 190 | t4() 191 | 192 | with pytest.raises(AttributeError): # AttributeError: 'tuple' object has no attribute 'append' 193 | t5() 194 | 195 | finally: 196 | Tag.STRICT_MODE=old 197 | 198 | 199 | 200 | if __name__=="__main__": 201 | # test_unparenting_clear() 202 | t0() -------------------------------------------------------------------------------- /test_init_render.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 -u 2 | # -*- coding: utf-8 -*- 3 | from typing import Type 4 | import pytest 5 | 6 | from htag import Tag,HTagException 7 | 8 | anon=lambda t: str(t).replace( str(id(t)),"*" ) 9 | 10 | 11 | ################################################################################################################ 12 | def test_simplified_init(): 13 | class Toto(Tag.div): pass 14 | assert str(Toto()) == '
' 15 | 16 | class Toto(Tag.div): 17 | def init(self,v,**a): 18 | self.add(v) 19 | assert str(Toto("hello")) == '
hello
' 20 | assert str(Toto("hello",_data_text='my')) == '
hello
' 21 | 22 | with pytest.raises(TypeError): # can't auto assign instance attribut with own simplified init() 23 | Toto(js="tag.focus()") 24 | 25 | class Toto(Tag.div): 26 | def init(self,v,vv=42,**a): 27 | self.add(v) 28 | self.add(vv) 29 | assert str(Toto("hello")) == '
hello42
' 30 | assert str(Toto("hello",43)) == '
hello43
' 31 | assert str(Toto("hello",vv=44)) == '
hello44
' 32 | 33 | assert str(Toto("hello",_class='my')) == '
hello42
' 34 | assert str(Toto("hello",_class='my',vv=45)) == '
hello45
' 35 | 36 | def test_own_render(): 37 | class Toto(Tag.div): 38 | def render(self): 39 | self.clear() 40 | self <= "own" 41 | assert str(Toto("hello")) == '
own
' 42 | 43 | class Toto(Tag.div): 44 | def init(self,nb): 45 | self.nb=nb 46 | def render(self): 47 | self.clear() 48 | self <= "*" * self.nb 49 | t=Toto(4) 50 | assert anon(t) == '
****
' 51 | t.nb=8 52 | assert anon(t) == '
********
' 53 | 54 | 55 | 56 | def test_weird_with_real_constructor(): 57 | class Toto(Tag.div): 58 | def __init__(self): 59 | super().__init__() 60 | assert str(Toto()) == '
' 61 | 62 | class Toto(Tag.div): 63 | def __init__(self): 64 | super().__init__(1) 65 | assert str(Toto()) == '
1
' 66 | 67 | class Toto(Tag.div): 68 | def __init__(self): 69 | super().__init__(1,2) 70 | 71 | with pytest.raises(TypeError): 72 | Toto() 73 | 74 | class Toto(Tag.div): 75 | def __init__(self): 76 | super().__init__(1,js="tag.focus()") 77 | 78 | t=Toto() 79 | assert str(t) == '
1
' 80 | assert t.js == "tag.focus()" 81 | 82 | 83 | if __name__=="__main__": 84 | 85 | import logging 86 | logging.basicConfig(format='[%(levelname)-5s] %(name)s: %(message)s',level=logging.DEBUG) 87 | # logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',level=logging.DEBUG) 88 | 89 | test_simplified_init() 90 | test_own_render() 91 | test_weird_with_real_constructor() 92 | -------------------------------------------------------------------------------- /test_main.py: -------------------------------------------------------------------------------- 1 | import sys,subprocess 2 | 3 | def test_main_help(): 4 | cmds=[sys.executable,"-m","htag","-h"] 5 | stdout = subprocess.run(cmds, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True).stdout 6 | assert "--gui" in stdout 7 | assert "--no-gui" in stdout 8 | -------------------------------------------------------------------------------- /test_new_events.py: -------------------------------------------------------------------------------- 1 | from htag import Tag 2 | from htag.tag import NotBindedCaller,Caller 3 | 4 | class T(Tag.div): 5 | pass 6 | 7 | def cb(o): 8 | print('k') 9 | 10 | 11 | def test_old_way_is_ok(): # <=0.7.4 12 | # CURRENT 13 | #======================================== 14 | t=T() 15 | t["onclick"] = t.bind(cb) + "var x=42" 16 | assert isinstance(t["onclick"],Caller) 17 | assert t["onclick"]._befores == [] 18 | assert t["onclick"]._afters == ["var x=42"] 19 | assert len(t["onclick"]._others ) == 0 20 | 21 | # ensure the base is ok 22 | t=T() 23 | t["onclick"] = "var x=42" 24 | assert isinstance(t["onclick"],str) 25 | 26 | t=T() 27 | t["onclick"] = t.bind(cb).bind(cb) + "var x=40" 28 | assert isinstance(t["onclick"],Caller) 29 | assert t["onclick"]._befores == [] 30 | assert t["onclick"]._afters == ["var x=40"] 31 | assert len(t["onclick"]._others ) == 1 32 | 33 | t=T() 34 | t["onclick"] = t.bind(cb) + "var x=40" 35 | assert isinstance(t["onclick"],Caller) 36 | assert t["onclick"]._befores == [] 37 | assert t["onclick"]._afters == ["var x=40"] 38 | assert len(t["onclick"]._others ) == 0 39 | 40 | t=T() 41 | t["onclick"] = "var x=40" + t.bind(cb) 42 | assert isinstance(t["onclick"],Caller) 43 | assert t["onclick"]._befores == ["var x=40"] 44 | assert t["onclick"]._afters == [] 45 | assert len(t["onclick"]._others ) == 0 46 | 47 | def test_new_events(): # >0.7.4 48 | # all "on*" attrs are not None, by default ... 49 | t=T() 50 | assert isinstance(t["onclick"],NotBindedCaller) 51 | 52 | # new syntax 53 | t=T() 54 | t["onclick"]+= "var x=2" 55 | assert isinstance(t["onclick"],NotBindedCaller) 56 | assert t["onclick"]._befores == [] 57 | assert t["onclick"]._afters == ["var x=2"] 58 | 59 | t=T() 60 | t["onclick"] = "var x=2" + t["onclick"] 61 | assert isinstance(t["onclick"],NotBindedCaller) 62 | assert t["onclick"]._befores == ["var x=2"] 63 | assert t["onclick"]._afters == [] 64 | 65 | t=T() 66 | t["onclick"] + "var x=43" # does nothing .. NON SENSE ! 67 | assert isinstance(t["onclick"],NotBindedCaller) 68 | assert t["onclick"]._befores == [] 69 | assert t["onclick"]._afters == [] 70 | 71 | # new syntax (over fucked ?!) (side effect, but works) 72 | t=T() 73 | t["onclick"].bind(cb).bind(cb) + "var x=41" 74 | assert isinstance(t["onclick"],Caller) 75 | assert t["onclick"]._befores == [] 76 | assert t["onclick"]._afters == ["var x=41"] 77 | assert len(t["onclick"]._others ) == 1 78 | 79 | def test_base(): 80 | def test(o): 81 | print("kkk") 82 | 83 | b=Tag.button("hello") 84 | b["onclick"] = test 85 | assert ' onclick="try{interact' in str(b) 86 | 87 | b=Tag.button("hello") 88 | b["onclick"] = b.bind( test ) 89 | assert ' onclick="try{interact' in str(b) 90 | 91 | b=Tag.button("hello") 92 | b["onclick"].bind( test ) 93 | assert ' onclick="try{interact' in str(b) 94 | 95 | if __name__=="__main__": 96 | 97 | import logging 98 | logging.basicConfig(format='[%(levelname)-5s] %(name)s: %(message)s',level=logging.DEBUG) 99 | -------------------------------------------------------------------------------- /test_placeholder.py: -------------------------------------------------------------------------------- 1 | from htag import Tag,HTagException 2 | from htag.render import HRenderer,Stater 3 | import asyncio 4 | from pprint import pprint 5 | 6 | import pytest 7 | 8 | import re 9 | def anon(x): 10 | return re.sub(r'id="\d+"','*id*',str(x)) 11 | 12 | def test_concept(): 13 | h= Tag.hello("world") 14 | t=Tag(Tag(Tag(Tag( h )))) 15 | assert str(h) == str(t) 16 | 17 | def test_concept_inherited(): 18 | class PlaceHolder(Tag): 19 | pass 20 | 21 | h= Tag.hello("world") 22 | t=PlaceHolder(PlaceHolder(PlaceHolder(PlaceHolder( h )))) 23 | assert str(h) == str(t) 24 | 25 | 26 | 27 | def test_base(): 28 | # test a placeholder 29 | B=Tag.B() 30 | 31 | s= Tag() # placeholder ! 32 | s += B 33 | 34 | # properties not available for placeholder 35 | with pytest.raises(HTagException): 36 | s["koko"]="kk" 37 | with pytest.raises(HTagException): 38 | v=s["koko"] 39 | 40 | with pytest.raises(HTagException): 41 | s.attrs["kkk"]=42 42 | 43 | with pytest.raises(HTagException): 44 | print(s.attrs) 45 | 46 | with pytest.raises(HTagException): 47 | print(s.bind) 48 | 49 | assert s.tag is None 50 | assert str(s) == "" 51 | assert s.innerHTML == "" 52 | assert s.childs[0] == B 53 | 54 | assert s._getTree() == { s: [ {B:[]} ]} 55 | 56 | assert "placeholder" in repr(s).lower() 57 | 58 | 59 | def test_tree(): 60 | 61 | # normal case (just verify all is ok) 62 | t=Tag.body() 63 | 64 | A= Tag.A() 65 | B= Tag.B() 66 | C= Tag.C() 67 | 68 | t <= A <= [B,C] 69 | 70 | assert t._getTree() == { 71 | t:[ {A: [{B:[]},{C:[]}]}] 72 | } 73 | 74 | 75 | # And now A is a placeholder 76 | t=Tag.body() 77 | 78 | A= Tag() 79 | B= Tag.B() 80 | C= Tag.C() 81 | 82 | t <= A <= [B,C] 83 | # assert t._getTree() == { # A is cleared from the tree 84 | # t:[{B:[]},{C:[]}] 85 | # } 86 | assert t._getTree() == { # A is in the tree 87 | t:[ {A: [{B:[]},{C:[]}]}] 88 | } 89 | 90 | 91 | def test_at_construction(): 92 | with pytest.raises(HTagException): 93 | Tag(_onclick="kkk") # can't autoset html attrs at construction time 94 | 95 | # but, can autoset instance properties at construction time 96 | t=Tag(value=42) 97 | assert t.value==42 98 | 99 | t=Tag(js=42) #TODO: SHOULD'NT BE POSSIBLE TOO ! 100 | assert t.js==42 101 | 102 | 103 | t=Tag( Tag.A()+Tag.B() ) 104 | assert str(t) == "" 105 | 106 | 107 | 108 | @pytest.mark.asyncio 109 | async def test_placeholder_to_body(): 110 | """ many js statements rendered """ 111 | 112 | class App(Tag): 113 | imports=[] # to avoid importing all scopes 114 | def init(self): 115 | self += "hello" 116 | 117 | #TODO: use Simu ;-) 118 | hr=HRenderer( App, "// my starter") 119 | 120 | # the placeholder is converted to a real body ! 121 | # to be interact'able, and have a consistence as a "main tag" 122 | assert hr.tag.tag == "body" 123 | 124 | # first interaction 125 | # r=await hr.interact(0,None,None,None,None) 126 | 127 | assert f'hello' in str(hr) 128 | 129 | 130 | def test_placeholder_mod_guess(): 131 | 132 | t=Tag.body() 133 | 134 | A= Tag() 135 | B= Tag.B() 136 | C= Tag.C() 137 | 138 | t <= A <= [B,C] 139 | 140 | s=Stater(t) 141 | 142 | print("BEFORE:",t) 143 | # A.clear() 144 | A<="hllll" 145 | print("AFTER: ",t) 146 | 147 | mod = s.guess() 148 | 149 | # IT SHOULD BE LIKE THIS !!!!! 150 | # IT SHOULD BE LIKE THIS !!!!! 151 | # IT SHOULD BE LIKE THIS !!!!! 152 | assert mod == [t] # body has changed ! 153 | # IT SHOULD BE LIKE THIS !!!!! 154 | # IT SHOULD BE LIKE THIS !!!!! 155 | # IT SHOULD BE LIKE THIS !!!!! 156 | 157 | 158 | if __name__=="__main__": 159 | 160 | import logging 161 | logging.basicConfig(format='[%(levelname)-5s] %(name)s: %(message)s',level=logging.DEBUG) 162 | logging.getLogger("htag.tag").setLevel( logging.ERROR ) 163 | 164 | # test_base() 165 | # test_tree() 166 | # test_at_construction() 167 | # asyncio.run( test_placeholder_to_body() ) 168 | # asyncio.run( test_using_a_placeholder() ) 169 | 170 | test_placeholder_mod_guess() -------------------------------------------------------------------------------- /test_runners.py: -------------------------------------------------------------------------------- 1 | import pytest,time 2 | import importlib 3 | from htag import Tag 4 | 5 | class MyApp(Tag.div): 6 | def init(self): 7 | self <= "Hello World" 8 | 9 | 10 | def test_default(): 11 | from htag.runners import Runner 12 | app=Runner( MyApp ) 13 | #TODO: test app.add_route( ) 14 | #TODO: test app.handle( ) 15 | 16 | def data_source(): 17 | for i in [ 18 | "DevApp", 19 | "BrowserStarletteHTTP", 20 | "BrowserStarletteWS", 21 | #"PyWebWiew", # before 0.8.0 (mispelling) 22 | "PyWebView", 23 | #"ChromeApp", # need Chrome installed ;-( 24 | #"WinApp", # need Chrome installed ;-( 25 | "AndroidApp", 26 | "BrowserTornadoHTTP", 27 | "PyScript", 28 | ]: 29 | yield i 30 | 31 | @pytest.mark.parametrize('my_runner', data_source()) 32 | def test_a_runner( my_runner ): 33 | mrunners=importlib.import_module("htag.runners") 34 | 35 | if hasattr(mrunners,my_runner): 36 | runner=getattr(mrunners,my_runner) 37 | r=runner( MyApp ) 38 | assert hasattr(r,"run") 39 | else: 40 | print("can't test %s" % my_runner) 41 | 42 | 43 | from htag.runners import commons 44 | 45 | 46 | def test_url2ak(): 47 | assert commons.url2ak("") == ( (),{} ) 48 | assert commons.url2ak("http://jojo.com/") == ( (),{} ) 49 | assert commons.url2ak("http://jojo.com/?") == ( (),{} ) 50 | assert commons.url2ak("http://jojo.com/???") == ( ("??",),{} ) 51 | 52 | assert commons.url2ak("http://jojo.com/?kiki") == ( ("kiki",),{} ) 53 | assert commons.url2ak("http://jojo.com/?kiki&koko") == ( ("kiki","koko"),{} ) 54 | assert commons.url2ak("http://jojo.com/?kiki&&&") == ( ("kiki",),{} ) 55 | assert commons.url2ak("http://jojo.com/?kiki???") == ( ("kiki???",),{} ) 56 | 57 | assert commons.url2ak("http://jojo.com/?1%202&kiki&a=3&b=5") == (('1 2', 'kiki'), {'a': '3', 'b': '5'}) 58 | 59 | # test a karg not valued 60 | assert commons.url2ak("http://jojo.com/?1%202&kiki&a=3&b=") == (('1 2', 'kiki'), {'a': '3', 'b': None}) 61 | 62 | # test an arg after kargs 63 | assert commons.url2ak("http://jojo.com/?1%202&kiki&a=3&b") == (('1 2', 'kiki', 'b'), {'a': '3'}) 64 | 65 | # test double kargs, the latest is the one 66 | assert commons.url2ak("http://jojo.com/?1%202&kiki&a=3&b=5&b=6") == (('1 2', 'kiki'), {'a': '3', 'b': '6'}) 67 | 68 | # test same ^^ url with anchor 69 | assert commons.url2ak("http://jojo.com/?1%202&kiki&a=3&b=5&b=6#yolo") == (('1 2', 'kiki'), {'a': '3', 'b': '6'}) 70 | 71 | 72 | def test_route_match(): 73 | assert commons.match( "/{val}","/jo" ) == {"val":"jo"} 74 | assert commons.match( "/{val:str}","/jo" ) == {"val":"jo"} 75 | assert commons.match( "/{v1}","/jo" ) == {"v1":"jo"} 76 | 77 | assert commons.match( "/item/{idx}","/item/1" ) == {"idx":"1"} 78 | assert commons.match( "/item/{idx:int}","/item/1" ) == {"idx":1} 79 | 80 | assert commons.match( "/download/{rest_of_path:path}","/download/pub/image.png" ) == {"rest_of_path":"pub/image.png"} 81 | 82 | 83 | assert not commons.match( "/xxx","/ppp" ) 84 | assert not commons.match( "/item/{idx}","/" ) 85 | assert not commons.match( "/item/{idx}","/item" ) 86 | assert not commons.match( "/item/{idx}","/item/" ) 87 | 88 | if __name__=="__main__": 89 | test_default() 90 | -------------------------------------------------------------------------------- /test_session_persitent.py: -------------------------------------------------------------------------------- 1 | from htag.runners.commons import SessionFile 2 | import os,pytest 3 | 4 | def test_SessionFile(): 5 | f="aeff.json" 6 | try: 7 | assert not os.path.isfile(f) 8 | 9 | s=SessionFile(f) 10 | s["hello"]=42 11 | s["hello"]+=1 12 | assert s["hello"]==43 13 | 14 | assert len(s)==1 15 | 16 | assert "jo" not in s 17 | 18 | with pytest.raises(Exception): 19 | s["jo"] 20 | 21 | with pytest.raises(Exception): 22 | del s["jo"] 23 | 24 | del s["hello"] 25 | 26 | assert not os.path.isfile(f) 27 | finally: 28 | if os.path.isfile(f): 29 | os.unlink(f) -------------------------------------------------------------------------------- /test_states_guesser.py: -------------------------------------------------------------------------------- 1 | from htag import Tag 2 | from htag.render import Stater 3 | 4 | import pytest 5 | 6 | @pytest.fixture(params=[1,2,3,4,5]) 7 | def env(request): 8 | A=Tag.A("a") 9 | B=Tag.B("b") 10 | C=Tag.C("c") 11 | D=Tag.D("d") 12 | 13 | A <= B <= C + D 14 | assert str(A)== "abcd" 15 | 16 | if request.param == 1: 17 | modmethod= lambda x: x<="mod" 18 | elif request.param == 2: 19 | modmethod= lambda x: x.__setitem__("class","mod") 20 | elif request.param == 3: 21 | modmethod= lambda x: x.clear() 22 | elif request.param == 4: 23 | modmethod= lambda x: x<=Tag.X("x") 24 | elif request.param == 5: 25 | modmethod= lambda x: x.__setitem__("class","mod") or x<="mod" 26 | 27 | return Stater(A),A,B,C,D,modmethod 28 | 29 | 30 | def test_no_mod(env): 31 | s,A,B,C,D,mod = env 32 | 33 | assert s.guess()==[] 34 | 35 | 36 | def test_mod_a_leaf(env): 37 | s,A,B,C,D,mod = env 38 | 39 | mod(C) 40 | assert s.guess()==[C] 41 | 42 | 43 | def test_mod_the_two_leaf(env): 44 | s,A,B,C,D,mod = env 45 | 46 | mod(D) 47 | mod(C) 48 | assert s.guess()==[C,D] 49 | 50 | def test_mod_the_two_leaf_in_other_order(env): 51 | s,A,B,C,D,mod = env 52 | 53 | mod(C) 54 | mod(D) 55 | assert s.guess()==[C,D] 56 | 57 | def test_mod_a_leaf_and_its_parent(env): 58 | s,A,B,C,D,mod = env 59 | 60 | mod(D) 61 | mod(B) 62 | assert s.guess()==[B] # just B (coz B include D) 63 | 64 | 65 | 66 | def test_mod_the_root(env): 67 | s,A,B,C,D,mod = env 68 | 69 | mod(A) 70 | assert s.guess()==[A] 71 | 72 | 73 | ######################### 74 | # same tests ^^but with placeholder in the middle 75 | ######################### 76 | 77 | 78 | @pytest.fixture(params=[1,3,4]) 79 | def env_placeholder(request): 80 | A=Tag.A("a") 81 | B=Tag("b") 82 | C=Tag.C("c") 83 | D=Tag.D("d") 84 | 85 | A <= B <= C + D 86 | assert str(A)== "abcd" 87 | 88 | if request.param == 1: 89 | modmethod= lambda x: x<="mod" 90 | elif request.param == 3: 91 | modmethod= lambda x: x.clear() 92 | elif request.param == 4: 93 | modmethod= lambda x: x<=Tag.X("x") 94 | 95 | return Stater(A),A,B,C,D,modmethod 96 | 97 | 98 | def test_ph_no_mod(env_placeholder): 99 | s,A,B,C,D,mod = env_placeholder 100 | 101 | assert s.guess()==[] 102 | 103 | 104 | def test_ph_mod_a_leaf(env_placeholder): 105 | s,A,B,C,D,mod = env_placeholder 106 | 107 | mod(C) 108 | assert s.guess()==[C] 109 | 110 | 111 | def test_ph_mod_the_two_leaf(env_placeholder): 112 | s,A,B,C,D,mod = env_placeholder 113 | 114 | mod(D) 115 | mod(C) 116 | assert s.guess()==[C,D] 117 | 118 | def test_ph_mod_the_two_leaf_in_other_order(env_placeholder): 119 | s,A,B,C,D,mod = env_placeholder 120 | 121 | mod(C) 122 | mod(D) 123 | assert s.guess()==[C,D] 124 | 125 | def test_ph_mod_a_leaf_and_its_parent(env_placeholder): 126 | s,A,B,C,D,mod = env_placeholder 127 | 128 | mod(D) 129 | mod(B) 130 | # assert s.guess()==[B] # just B (coz B include D) 131 | assert s.guess()==[A] # just B (coz B include D) 132 | 133 | def test_ph_mod_the_root(env_placeholder): 134 | s,A,B,C,D,mod = env_placeholder 135 | 136 | mod(A) 137 | assert s.guess()==[A] 138 | 139 | 140 | if __name__=="__main__": 141 | pytest.main() 142 | -------------------------------------------------------------------------------- /test_statics.py: -------------------------------------------------------------------------------- 1 | from htag import Tag,HTagException 2 | from htag.render import HRenderer 3 | import pytest 4 | 5 | def test_statics_only_tagbase(): 6 | class AEFF(Tag): 7 | statics="body {background:red}", b"alert(42);" 8 | 9 | h=str(HRenderer(AEFF,"//js")) 10 | 11 | assert "" in h 12 | assert "" in h 13 | 14 | del AEFF 15 | 16 | 17 | def test_built_immediatly(): 18 | ################################################################ 19 | # test static discovering (in built immediatly) 20 | ################################################################ 21 | class O(Tag.div): 22 | statics=Tag.style("/*S1*/") 23 | 24 | assert "/*S1*/" in str(HRenderer( O, "//")) 25 | ################################################################ 26 | class OO1(Tag.div): 27 | imports = O 28 | def init(self): 29 | self <= O() # "O" is a direct child 30 | 31 | assert "/*S1*/" in str(HRenderer( OO1, "//")) 32 | # ################################################################ 33 | class OO2(Tag.div): 34 | imports = O 35 | def init(self): 36 | self <= Tag.div( O() ) # "O" is a non-direct child 37 | 38 | assert "/*S1*/" in str(HRenderer( OO2, "//")) 39 | ################################################################ 40 | 41 | def test_build_lately(): 42 | ################################################################ 43 | # test static discovering (in built lately) 44 | ################################################################ 45 | class O(Tag.div): 46 | statics=Tag.style("/*S1*/") 47 | 48 | assert "/*S1*/" in str(HRenderer( O, "//")) 49 | ################################################################ 50 | class OO1(Tag.div): 51 | imports = O 52 | def render(self): 53 | self.clear() 54 | self <= O() # "O" is a direct child 55 | 56 | assert "/*S1*/" in str(HRenderer( OO1, "//")) 57 | ################################################################ 58 | class OO2(Tag.div): 59 | imports = O 60 | def render(self): 61 | self.clear() 62 | self <= Tag.div( O() ) # "O" is a non-direct child 63 | 64 | assert "/*S1*/" in str(HRenderer( OO2, "//")) 65 | ################################################################ 66 | 67 | # def test_TagBase_md5(): 68 | 69 | # sameContent="hello" 70 | # sameattrs=dict(_class="hello") 71 | # t1=Tag.a(sameContent,**sameattrs) 72 | # t2=Tag.a(sameContent,**sameattrs) 73 | 74 | # assert t1.md5 == t2.md5 75 | 76 | # def test_Tag_md5(): 77 | # class My(Tag.div): 78 | # def __init__(self,txt,**a): 79 | # Tag.div.__init__(self,**a) 80 | # self <= txt 81 | 82 | # sameContent="hello" 83 | # sameattrs=dict(_class="hello") 84 | # t1=My(sameContent,**sameattrs) 85 | # t2=My(sameContent,**sameattrs) 86 | 87 | # #md5 is computed, but not useful 88 | # #(as it's only for tagbase in statics) 89 | # assert t1.md5 != t2.md5 # so, it's different 90 | 91 | def test_doubbles_statics(): 92 | class AppSS(Tag.div): 93 | statics = "kiki","kiki" 94 | imports=[] # just to avoid import all Tag in the scoped process 95 | def init(self,m="default"): 96 | self <= m 97 | self <= Tag.span("world") 98 | 99 | hr1=HRenderer(AppSS,"") 100 | assert len(hr1._statics)==2 # 2 real statics 101 | assert str(hr1).count("")==1 # but just one rendered (coz they are identicals (_hash_)) 102 | 103 | class AppST(Tag.div): 104 | statics = Tag.style("kiki"),Tag.style("kiki") 105 | imports=[] # just to avoid import all Tag in the scoped process 106 | def init(self,m="default"): 107 | self <= m 108 | self <= Tag.span("world") 109 | 110 | hr2=HRenderer(AppST,"") 111 | assert len(hr2._statics)==2 # 2 real statics 112 | assert str(hr2).count("")==1 # but just one rendered (coz they are identicals (_hash_)) 113 | 114 | def test_inherit_bases(): 115 | class A(Tag): 116 | statics = "StylesA" 117 | imports=[] 118 | 119 | class B(A): 120 | statics = "StylesB" 121 | imports=[] 122 | 123 | hr=HRenderer(A,"") 124 | styles=[i for i in hr._statics if i.tag=="style"] 125 | assert len(styles)==1 126 | 127 | hr=HRenderer(B,"") 128 | styles=[i for i in hr._statics if i.tag=="style"] 129 | assert len(styles)==2 130 | 131 | 132 | if __name__=="__main__": 133 | 134 | import logging 135 | logging.basicConfig(format='[%(levelname)-5s] %(name)s: %(message)s',level=logging.DEBUG) 136 | # logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',level=logging.DEBUG) 137 | # test_Tag_md5() 138 | # test_statics_only_tagbase() 139 | test_built_immediatly() -------------------------------------------------------------------------------- /test_tag_tech.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 -u 2 | # -*- coding: utf-8 -*- 3 | from unittest.util import strclass 4 | import pytest 5 | 6 | from htag import Tag,HTagException 7 | from htag.tag import Elements,NotBindedCaller,Caller,BaseCaller 8 | 9 | ################################################################################################################ 10 | def test_Elements(): 11 | x=Elements() 12 | assert str(x)=="" 13 | assert repr(x).startswith(" 0.10.0) 12 | 13 | @pytest.mark.asyncio 14 | async def test_update_default(): 15 | class MyTag(Tag.div): 16 | def init(self): 17 | pass 18 | 19 | hr=HRenderer( MyTag, "//") 20 | 21 | tag=hr.tag 22 | assert not await tag.update() 23 | 24 | 25 | @pytest.mark.asyncio 26 | async def test_update_capable(): 27 | class MyTag(Tag.div): 28 | def init(self): 29 | pass 30 | 31 | async def _sendactions(actions:dict) -> bool: 32 | assert "update" in actions 33 | assert "post" in actions 34 | ll=list(actions["update"].items()) 35 | assert len(ll)==1 36 | id,content = ll[0] 37 | assert str(id) in content 38 | assert ">hello<" in content 39 | return True 40 | 41 | hr=HRenderer( MyTag, "//") 42 | hr.sendactions = _sendactions 43 | 44 | tag=hr.tag 45 | tag+="hello" 46 | tag.js="console.log(42)" # add a js/post 47 | assert await tag.update() 48 | 49 | 50 | 51 | 52 | if __name__=="__main__": 53 | import logging 54 | logging.basicConfig(format='[%(levelname)-5s] %(name)s: %(message)s',level=logging.DEBUG) 55 | # asyncio.run( test_update_default() ) 56 | asyncio.run( test_update_capable() ) --------------------------------------------------------------------------------