├── docs
├── README.md
├── htag.png
├── js_bidirectionnal.md
├── tag_update.md
├── query_params.md
├── htag.svg
├── index.md
├── concepts.md
├── hrenderer.md
└── runners.md
├── old_runners
├── README.md
├── browserstarlettehttp.py
├── browsertornadohttp.py
├── browserstarlettews.py
├── browserhttp.py
└── winapp.py
├── examples
├── 7guis
│ ├── README.md
│ ├── gui1.py
│ ├── gui3.py
│ ├── gui2.py
│ ├── gui4.py
│ └── gui5.py
├── hello_world.py
├── autoreload.py
├── pyscript.html
├── README.md
├── matplot.py
├── pyscript_htagui.html
├── leaflet.py
├── new_timer.py
├── pyscript_matplotlib.html
├── camshot.py
├── calc.py
├── pyscript_with_hashchange.html
├── navigate_with_hashchange.py
├── ace_editor.py
├── stream.py
├── todomvc.py
├── demo.py
├── pyscript_htbulma.html
├── app.py
└── htag_with_state_manager.py
├── htag
├── ui.py
├── __init__.py
├── runners
│ ├── pywebview.py
│ ├── pyscript.py
│ ├── commons
│ │ └── __init__.py
│ └── chromeappmode.py
├── attrs.py
└── __main__.py
├── .vscode
├── settings.json
└── tasks.json
├── .github
└── workflows
│ ├── on_commit_do_deploy_mkdocs.yml
│ ├── test_platforms.yml
│ ├── on_commit_do_all_unittests.yml
│ ├── update_version.py
│ ├── selenium.yaml
│ └── on_tag_do_release_and_deploy_pypi.yml
├── manual_tests_base.py
├── selenium
├── run.py
├── app1.py
├── app3.py
├── app2.py
├── tests.py
├── app4.py
└── hclient.py
├── test_session_persitent.py
├── mkdocs.yml
├── manual_tests_new_InternalCall.py
├── manual_tests_expose.py
├── LICENSE
├── test_update.py
├── manual_tests_persitent.py
├── pyproject.toml
├── manual_tests_remove.py
├── test_ui.py
├── manual_tests_htbulma.py
├── .gitignore
├── test_init_render.py
├── test_new_events.py
├── brython
├── example1.html
├── example2.html
└── README.md
├── manual_tests_events.py
├── test_constructors.py
├── manual_tests_qp.py
├── test_states_guesser.py
├── test_pyscript_runner.py
├── test_placeholder.py
├── test_chromeappmode_runner.py
├── test_statics.py
├── _pyscript_dev.html
└── test_dom.py
/docs/README.md:
--------------------------------------------------------------------------------
1 | See docs here :
2 | https://manatlan.github.io/htag/
3 |
--------------------------------------------------------------------------------
/docs/htag.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/manatlan/htag/HEAD/docs/htag.png
--------------------------------------------------------------------------------
/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"
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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@v4
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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)
--------------------------------------------------------------------------------
/.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 | runs-on: ${{ matrix.os }}
16 | steps:
17 | - uses: actions/checkout@v4
18 | - uses: actions/setup-python@v5
19 | with:
20 | python-version: ${{ matrix.python-version }}
21 | - name: run pytest
22 | run: |
23 | python -m pip install uv
24 | uv sync --dev
25 | uv run pytest .
26 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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@v4
21 | - name: Set up Python ${{ matrix.python-version }}
22 | uses: actions/setup-python@v5
23 | with:
24 | python-version: ${{ matrix.python-version }}
25 | - name: Install dependencies
26 | run: |
27 | python -m pip install uv
28 | uv sync --dev
29 | - name: Test with pytest
30 | run: |
31 | uv run pytest . -s
32 |
--------------------------------------------------------------------------------
/.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}' !")
--------------------------------------------------------------------------------
/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/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()
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/examples/pyscript.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Test htag
7 |
8 |
9 |
10 |
11 | loading...
12 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/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 "uv build" before !!!!
36 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 "uv build" before !!!!
42 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 "uv build" before !!!!
42 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/pyscript_htagui.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Test htag
7 |
8 |
9 |
10 |
11 | loading...
12 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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": "uv run pytest --cov-report html --cov=htag . && google-chrome htmlcov/index.html",
10 | "problemMatcher": [],
11 | "presentation": {
12 | "panel": "new",
13 | "focus": true
14 | },
15 | "group": {
16 | "kind": "build",
17 | "isDefault": true
18 | }
19 | },
20 | {
21 | "label": "clean repo",
22 | "type": "shell",
23 | "command": "rm -rf build __pycache__ htag/__pycache__ htag/runners/__pycache__ .pytest_cache .coverage htmlcov",
24 | "problemMatcher": [],
25 | "presentation": {
26 | "panel": "new",
27 | "focus": true
28 | }
29 | },
30 | {
31 | "label": "Build zip with all",
32 | "type": "shell",
33 | "command": "rm -rf build __pycache__ htag/__pycache__ htag/runners/__pycache__ .pytest_cache .coverage htmlcov; zip -r src.zip htag examples test_*",
34 | "problemMatcher": [],
35 | "presentation": {
36 | "panel": "new",
37 | "focus": true
38 | }
39 | }
40 | ]
41 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/test_update.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3 -u
2 | # -*- coding: utf-8 -*-
3 | from dataclasses import replace
4 | import pytest
5 | import asyncio
6 | import re
7 |
8 | from htag import Tag,HTagException
9 | from htag.render import Stater,HRenderer,fmtcaller
10 |
11 | # Test new feature tag.update() with compatible hrenderer/runner (for htag > 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() )
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
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 = [{name = "manatlan", email = "manatlan@gmail.com"}]
6 | readme = "README.md"
7 | license = {text = "MIT"}
8 | requires-python = ">=3.7"
9 | keywords = [
10 | "gui", "electron", "cef", "pywebview", "starlette", "uvicorn",
11 | "tornado", "asyncio", "desktop", "web", "mobile", "http",
12 | "websocket", "html", "pyscript", "android", "kivy", "apk"
13 | ]
14 | classifiers = [
15 | "Operating System :: OS Independent",
16 | "Topic :: Software Development :: Libraries :: Python Modules",
17 | "Topic :: Software Development :: Build Tools",
18 | "License :: OSI Approved :: Apache Software License",
19 | ]
20 |
21 | [project.urls]
22 | Homepage = "https://github.com/manatlan/htag"
23 | Repository = "https://github.com/manatlan/htag"
24 | Documentation = "https://manatlan.github.io/htag/"
25 |
26 | [project.optional-dependencies]
27 | dev = [
28 | "pytest>=6",
29 | "pytest-cov>=3",
30 | "pytest-asyncio>=0.19",
31 | "pywebview>=3.6",
32 | "selenium>=4",
33 | "fake-winreg>=1.6",
34 | ]
35 |
36 | [build-system]
37 | requires = ["hatchling"]
38 | build-backend = "hatchling.build"
39 |
40 | [project.scripts]
41 | htag = "htag.__main__:command"
42 |
43 | [tool.uv]
44 | dev-dependencies = [
45 | "pytest>=6",
46 | "pytest-cov>=3",
47 | "pytest-asyncio>=0.19",
48 | "pywebview>=3.6",
49 | "selenium>=4",
50 | "fake-winreg>=1.6",
51 | ]
52 |
53 | [tool.pytest.ini_options]
54 | addopts = "--cov=htag --cov-report=html --cov-report=xml --cov-report=term-missing"
55 |
--------------------------------------------------------------------------------
/examples/pyscript_matplotlib.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Test htag
7 |
8 |
9 |
10 |
11 | loading...
12 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/test_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 | import sys
10 | import types
11 | import pytest
12 |
13 | @pytest.fixture(autouse=True)
14 | def cleanup_imports():
15 | """Fixture to ensure a clean slate for imports for each test."""
16 | original_modules = dict(sys.modules)
17 |
18 | # Unload modules that might interfere
19 | for mod in ['htag.ui', 'htagui', 'htagui.basics']:
20 | if mod in sys.modules:
21 | del sys.modules[mod]
22 |
23 | yield
24 |
25 | # Restore original modules
26 | sys.modules.clear()
27 | sys.modules.update(original_modules)
28 |
29 |
30 | def test_ui_import_from_basics_jules():
31 | """ Test if 'htag.ui' imports from 'htagui.basics' when available """
32 | # Create fake modules
33 | htagui_module = types.ModuleType('htagui')
34 | basics_module = types.ModuleType('htagui.basics')
35 | basics_module.MyWidget = 'BasicsWidget'
36 |
37 | # Add to sys.modules
38 | sys.modules['htagui'] = htagui_module
39 | sys.modules['htagui.basics'] = basics_module
40 |
41 | # Import htag.ui and test
42 | import htag.ui
43 | assert hasattr(htag.ui, 'MyWidget')
44 | assert htag.ui.MyWidget == 'BasicsWidget'
45 |
46 |
47 | def test_ui_import_fallback_to_htagui_jules():
48 | """ Test if 'htag.ui' falls back to importing from 'htagui' """
49 | # Create a fake htagui module (without .basics)
50 | htagui_module = types.ModuleType('htagui')
51 | htagui_module.MyWidget = 'HtaguiWidget'
52 |
53 | # Add to sys.modules
54 | sys.modules['htagui'] = htagui_module
55 |
56 | # Import htag.ui and test
57 | import htag.ui
58 | assert hasattr(htag.ui, 'MyWidget')
59 | assert htag.ui.MyWidget == 'HtaguiWidget'
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/examples/pyscript_with_hashchange.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Test htag
7 |
8 |
9 |
10 |
11 | loading...
12 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/.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/uv packages
30 | run: pip install webdriver-manager selenium uv
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: uv 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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.github/workflows/on_tag_do_release_and_deploy_pypi.yml:
--------------------------------------------------------------------------------
1 | # This workflow will upload a Python Package to PyPI when a release is created
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
3 |
4 | # This workflow uses actions that are not certified by GitHub.
5 | # They are provided by a third-party and are governed by
6 | # separate terms of service, privacy policy, and support
7 | # documentation.
8 |
9 | name: Upload Python Package
10 |
11 | on:
12 | # workflow_dispatch: # to be able to test manually on github/actions
13 | # release:
14 | # types: [published]
15 | push:
16 | tags:
17 | - "v*.*.*"
18 |
19 | permissions:
20 | contents: read
21 |
22 | jobs:
23 | release-build:
24 | runs-on: ubuntu-latest
25 |
26 | steps:
27 | - uses: actions/checkout@v4
28 |
29 | - uses: actions/setup-python@v5
30 | with:
31 | python-version: "3.x"
32 |
33 | - name: Build release distributions
34 | run: |
35 | # NOTE: put your own distribution build steps here.
36 | python -m pip install uv
37 | uv sync --dev
38 | python .github/workflows/update_version.py ${{github.ref_name}}
39 | uv build
40 |
41 | - name: Upload distributions
42 | uses: actions/upload-artifact@v4
43 | with:
44 | name: release-dists
45 | path: dist/
46 |
47 | pypi-publish:
48 | runs-on: ubuntu-latest
49 | needs:
50 | - release-build
51 | permissions:
52 | # IMPORTANT: this permission is mandatory for trusted publishing
53 | id-token: write
54 |
55 | # Dedicated environments with protections for publishing are strongly recommended.
56 | # For more information, see: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#deployment-protection-rules
57 | environment:
58 | name: pypi
59 | # OPTIONAL: uncomment and update to include your PyPI project URL in the deployment status:
60 | # url: https://pypi.org/p/YOURPROJECT
61 | #
62 | # ALTERNATIVE: if your GitHub Release name is the PyPI project version string
63 | # ALTERNATIVE: exactly, uncomment the following line instead:
64 | # url: https://pypi.org/project/YOURPROJECT/${{ github.event.release.name }}
65 |
66 | steps:
67 | - name: Retrieve release distributions
68 | uses: actions/download-artifact@v4
69 | with:
70 | name: release-dists
71 | path: dist/
72 |
73 | - name: Publish release distributions to PyPI
74 | uses: pypa/gh-action-pypi-publish@release/v1
75 | with:
76 | packages-dir: dist/
--------------------------------------------------------------------------------
/docs/htag.svg:
--------------------------------------------------------------------------------
1 |
2 |
71 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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)
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/brython/example1.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Example 1
5 |
6 |
7 |
10 |
15 |
16 |
17 |
18 |
31 |
32 |
33 |
34 |
35 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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()
--------------------------------------------------------------------------------
/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