tealui 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tealui/__init__.py +43 -0
- tealui/__main__.py +4 -0
- tealui/cli.py +173 -0
- tealui/core.py +113 -0
- tealui/devtools.py +126 -0
- tealui/fonts/LICENSES.txt +287 -0
- tealui/fonts/inter.woff2 +0 -0
- tealui/fonts/mono.woff2 +0 -0
- tealui/fonts/serif-italic.woff2 +0 -0
- tealui/fonts/serif.woff2 +0 -0
- tealui/gallery.py +81 -0
- tealui/icons-LICENSE.txt +43 -0
- tealui/icons.py +153 -0
- tealui/look.py +153 -0
- tealui/lookcli.py +217 -0
- tealui/looks/__init__.py +7 -0
- tealui/looks/accents.py +18 -0
- tealui/looks/full.py +163 -0
- tealui/looks/paper.py +23 -0
- tealui/looks/teal.py +352 -0
- tealui/page.py +87 -0
- tealui/py.typed +0 -0
- tealui/tags.py +112 -0
- tealui/theme.py +184 -0
- tealui/toolchain.py +246 -0
- tealui/ui/__init__.py +1 -0
- tealui/ui/article.py +103 -0
- tealui/ui/charts.py +111 -0
- tealui/ui/content.py +76 -0
- tealui/ui/data.py +96 -0
- tealui/ui/forms.py +92 -0
- tealui/ui/forms2.py +150 -0
- tealui/ui/foundation.py +66 -0
- tealui/ui/mobile.py +58 -0
- tealui/ui/nav.py +58 -0
- tealui/ui/overlay.py +93 -0
- tealui/ui/shell.py +85 -0
- tealui/versions.py +7 -0
- tealui-0.3.0.dist-info/METADATA +257 -0
- tealui-0.3.0.dist-info/RECORD +44 -0
- tealui-0.3.0.dist-info/WHEEL +5 -0
- tealui-0.3.0.dist-info/entry_points.txt +2 -0
- tealui-0.3.0.dist-info/licenses/LICENSE +21 -0
- tealui-0.3.0.dist-info/top_level.txt +1 -0
tealui/__init__.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""A tiny, clean, pythonic UI library."""
|
|
2
|
+
from . import tags as _tags
|
|
3
|
+
from .core import El, Tag, Raw, render
|
|
4
|
+
from .tags import *
|
|
5
|
+
from .look import use, using, current, r, make_look, check_look, look_problems
|
|
6
|
+
from .looks import (TEAL, PAPER, SLATE, INDIGO, MIDNIGHT, LEDGER, ENTERPRISE, CONTRAST,
|
|
7
|
+
FOREST, SUNSET, NORDIC, SOFT, TERMINAL, BRUTAL)
|
|
8
|
+
from .theme import with_accent, with_fonts
|
|
9
|
+
from .icons import register_icon
|
|
10
|
+
from .page import Page
|
|
11
|
+
from .ui.foundation import Icon, Button, Badge, Avatar, Card, Kbd
|
|
12
|
+
from .ui.charts import Sparkline, AreaChart
|
|
13
|
+
from .ui.data import Delta, Stat, Progress, Table, BarList
|
|
14
|
+
from .ui.nav import Tabs, Breadcrumb
|
|
15
|
+
from .ui.shell import NavItem, Sidebar, Topbar, PageHeader, DarkToggle, Shell
|
|
16
|
+
from .ui.forms import Segmented, SearchBox, TagFilter, tag_show
|
|
17
|
+
from .ui.content import SiteHeader, Prose, CodeBlock, Callout
|
|
18
|
+
from .ui.forms2 import TextInput, SelectBox, Checkbox, Switch, Field, TextBox, RadioGroup
|
|
19
|
+
from .ui.overlay import Modal, Dropdown, Toaster, toast_js
|
|
20
|
+
from .ui.mobile import AppScreen, LargeTitle, ListGroup, ListRow, TabBar
|
|
21
|
+
from .ui.article import Toc, ReadingProgress, ArticleHeader, ShareBar, PostCard, Pager, DocsNav
|
|
22
|
+
use(TEAL)
|
|
23
|
+
__all__ = list(dict.fromkeys([
|
|
24
|
+
"El", "Tag", "Raw", "render", "use", "using", "current", "r",
|
|
25
|
+
"TEAL", "PAPER", "SLATE", "INDIGO", "MIDNIGHT", "LEDGER", "ENTERPRISE", "CONTRAST",
|
|
26
|
+
"FOREST", "SUNSET", "NORDIC", "SOFT", "TERMINAL", "BRUTAL",
|
|
27
|
+
"make_look", "check_look", "look_problems", "with_accent", "Page", "Icon",
|
|
28
|
+
"Button", "Badge", "Avatar", "Card", "Kbd",
|
|
29
|
+
"Sparkline", "AreaChart", "Delta", "Stat", "Progress", "Table", "BarList",
|
|
30
|
+
"Tabs", "Breadcrumb",
|
|
31
|
+
"NavItem", "Sidebar", "Topbar", "PageHeader", "DarkToggle", "Shell",
|
|
32
|
+
"Segmented", "SearchBox",
|
|
33
|
+
"SiteHeader", "Prose", "CodeBlock", "Callout",
|
|
34
|
+
"TextInput", "SelectBox", "Checkbox", "Switch", "Field",
|
|
35
|
+
"Modal",
|
|
36
|
+
"Dropdown", "Toaster", "toast_js",
|
|
37
|
+
"Toc", "ReadingProgress", "ArticleHeader", "ShareBar", "PostCard", "Pager", "DocsNav",
|
|
38
|
+
"AppScreen", "LargeTitle", "ListGroup", "ListRow", "TabBar",
|
|
39
|
+
"with_fonts", "register_icon",
|
|
40
|
+
"TextBox", "RadioGroup", "TagFilter", "tag_show",
|
|
41
|
+
*_tags.__all__
|
|
42
|
+
]))
|
|
43
|
+
__version__ = "0.3.0"
|
tealui/__main__.py
ADDED
tealui/cli.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .theme import tailwind_input
|
|
10
|
+
from .versions import TAILWIND_RANGE, ALPINE_RANGE, HTMX_RANGE
|
|
11
|
+
from .devtools import _run_test, _run_export
|
|
12
|
+
from . import toolchain
|
|
13
|
+
from . import lookcli
|
|
14
|
+
|
|
15
|
+
_FONTS = ("inter.woff2", "serif.woff2", "serif-italic.woff2", "mono.woff2")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _fonts_dir():
|
|
19
|
+
return Path(__file__).resolve().parent / "fonts"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _write_input_and_fonts(out, srcs, input_path, looks=()):
|
|
23
|
+
out_dir = Path(out).parent
|
|
24
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
abs_paths = [Path(p).resolve().as_posix() for p in (srcs or [os.getcwd()])]
|
|
26
|
+
if looks:
|
|
27
|
+
# Third-party looks may live outside the scanned folders (site-packages): list their classes in a file.
|
|
28
|
+
looks_txt = out_dir / "tealui.looks.txt"
|
|
29
|
+
looks_txt.write_text(lookcli.classes(looks), encoding="utf-8")
|
|
30
|
+
abs_paths.append(looks_txt.resolve().as_posix())
|
|
31
|
+
print(f"included {len(looks)} look(s): {', '.join(x.get('name', '?') for x in looks)}")
|
|
32
|
+
if input_path is None:
|
|
33
|
+
input_path = out_dir / "tealui.input.css"
|
|
34
|
+
input_path = Path(input_path)
|
|
35
|
+
input_path.parent.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
input_path.write_text(tailwind_input(*abs_paths), encoding="utf-8")
|
|
37
|
+
fonts_out = out_dir / "fonts"
|
|
38
|
+
fonts_out.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
for name in _FONTS:
|
|
40
|
+
shutil.copyfile(_fonts_dir() / name, fonts_out / name)
|
|
41
|
+
return input_path
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _run_css(args):
|
|
45
|
+
input_path = _write_input_and_fonts(args.out, args.src, args.input, lookcli.build_looks(args.look))
|
|
46
|
+
print(f"wrote {input_path}")
|
|
47
|
+
print(f"copied fonts to {Path(args.out).parent / 'fonts'}")
|
|
48
|
+
print(f"next: tealui build {args.out} --src <your source folders>")
|
|
49
|
+
print(f"or run any Tailwind v4 CLI: tailwindcss -i {input_path} -o {args.out} --minify")
|
|
50
|
+
return 0
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _run_build(args):
|
|
54
|
+
cache = toolchain.cache_root(args.cache)
|
|
55
|
+
input_path = _write_input_and_fonts(args.out, args.src, None, lookcli.build_looks(args.look))
|
|
56
|
+
exe = toolchain.ensure_tailwind(cache, TAILWIND_RANGE)
|
|
57
|
+
if args.alpine:
|
|
58
|
+
alpine_path = toolchain.ensure_alpine(cache, ALPINE_RANGE)
|
|
59
|
+
dest = Path(args.alpine)
|
|
60
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
shutil.copyfile(alpine_path, dest)
|
|
62
|
+
version = toolchain.installed(cache, "alpine", ALPINE_RANGE)
|
|
63
|
+
print(f"copied Alpine {version} to {args.alpine}")
|
|
64
|
+
if args.htmx:
|
|
65
|
+
htmx_path = toolchain.ensure_htmx(cache, HTMX_RANGE)
|
|
66
|
+
dest = Path(args.htmx)
|
|
67
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
shutil.copyfile(htmx_path, dest)
|
|
69
|
+
version = toolchain.installed(cache, "htmx", HTMX_RANGE)
|
|
70
|
+
print(f"copied HTMX {version} to {args.htmx}")
|
|
71
|
+
cmd = [str(exe), "-i", str(input_path), "-o", str(Path(args.out).resolve()), "--minify"]
|
|
72
|
+
if args.watch:
|
|
73
|
+
cmd.append("--watch")
|
|
74
|
+
return subprocess.run(cmd).returncode
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _run_update(args):
|
|
78
|
+
cache = toolchain.cache_root(args.cache)
|
|
79
|
+
for name, major, ensure_fn in [("tailwind", TAILWIND_RANGE, toolchain.ensure_tailwind),
|
|
80
|
+
("alpine", ALPINE_RANGE, toolchain.ensure_alpine),
|
|
81
|
+
("htmx", HTMX_RANGE, toolchain.ensure_htmx)]:
|
|
82
|
+
before = toolchain.installed(cache, name, major)
|
|
83
|
+
ensure_fn(cache, major, update=True)
|
|
84
|
+
after = toolchain.installed(cache, name, major)
|
|
85
|
+
pkg_name = {"tailwind": "tailwindcss", "alpine": "alpinejs", "htmx": "htmx.org"}[name]
|
|
86
|
+
if before is None:
|
|
87
|
+
print(f"{pkg_name} installed {after}")
|
|
88
|
+
elif before != after:
|
|
89
|
+
print(f"{pkg_name} {before} -> {after}")
|
|
90
|
+
else:
|
|
91
|
+
print(f"{pkg_name} {after} (up to date)")
|
|
92
|
+
return 0
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _run_versions(args):
|
|
96
|
+
cache = toolchain.cache_root(args.cache)
|
|
97
|
+
for pkg, name, major in [("tailwindcss", "tailwind", TAILWIND_RANGE), ("alpinejs", "alpine", ALPINE_RANGE), ("htmx.org", "htmx", HTMX_RANGE)]:
|
|
98
|
+
inst = toolchain.installed(cache, name, major)
|
|
99
|
+
if inst:
|
|
100
|
+
inst_str = f"installed {inst}"
|
|
101
|
+
else:
|
|
102
|
+
inst_str = "not installed"
|
|
103
|
+
try:
|
|
104
|
+
if pkg == "tailwindcss":
|
|
105
|
+
latest = toolchain.latest_tailwind(major)
|
|
106
|
+
else:
|
|
107
|
+
latest, _ = toolchain.npm_latest(pkg, major)
|
|
108
|
+
latest_str = latest or "unknown"
|
|
109
|
+
except toolchain.ToolchainError:
|
|
110
|
+
latest_str = "unknown"
|
|
111
|
+
print(f"{pkg} {inst_str} latest {latest_str} tracking {major}.x")
|
|
112
|
+
try:
|
|
113
|
+
dist_tags = json.loads(toolchain._get(f"https://registry.npmjs.org/-/package/{pkg}/dist-tags"))
|
|
114
|
+
overall_latest = dist_tags.get("latest")
|
|
115
|
+
if overall_latest and not overall_latest.startswith(f"{major}."):
|
|
116
|
+
print(f"note: a newer major version exists ({overall_latest}); TealUI keeps tracking {major}.x until it has been tested")
|
|
117
|
+
except (toolchain.ToolchainError, json.JSONDecodeError, KeyError):
|
|
118
|
+
pass
|
|
119
|
+
return 0
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def main(argv=None):
|
|
123
|
+
"""Run the tealui command line interface."""
|
|
124
|
+
parser = argparse.ArgumentParser(prog="tealui")
|
|
125
|
+
sub = parser.add_subparsers(dest="cmd")
|
|
126
|
+
css = sub.add_parser("css", help="write the Tailwind input file and copy fonts")
|
|
127
|
+
css.add_argument("out")
|
|
128
|
+
css.add_argument("--src", action="append", default=[])
|
|
129
|
+
css.add_argument("--input")
|
|
130
|
+
css.add_argument("--look", action="append", default=[], help="third-party look to include (module:NAME)")
|
|
131
|
+
build = sub.add_parser("build", help="write inputs, then run the Tailwind CLI")
|
|
132
|
+
build.add_argument("out")
|
|
133
|
+
build.add_argument("--src", action="append", default=[])
|
|
134
|
+
build.add_argument("--cache")
|
|
135
|
+
build.add_argument("--watch", action="store_true")
|
|
136
|
+
build.add_argument("--alpine")
|
|
137
|
+
build.add_argument("--htmx")
|
|
138
|
+
build.add_argument("--look", action="append", default=[], help="third-party look to include (module:NAME)")
|
|
139
|
+
upd = sub.add_parser("update", help="update the cached toolchain")
|
|
140
|
+
upd.add_argument("--cache")
|
|
141
|
+
ver = sub.add_parser("versions", help="show installed and latest versions")
|
|
142
|
+
ver.add_argument("--cache")
|
|
143
|
+
test = sub.add_parser("test", help="run the unit tests")
|
|
144
|
+
test.add_argument("--browser", action="store_true")
|
|
145
|
+
export = sub.add_parser("export", help="build wheel, sdist and source zip")
|
|
146
|
+
export.add_argument("outdir", nargs="?", default="dist")
|
|
147
|
+
export.add_argument("--no-test", action="store_true")
|
|
148
|
+
export.add_argument("--wheel-only", action="store_true")
|
|
149
|
+
lookcli.add_parsers(sub)
|
|
150
|
+
args = parser.parse_args(argv)
|
|
151
|
+
try:
|
|
152
|
+
if args.cmd == "css":
|
|
153
|
+
return _run_css(args)
|
|
154
|
+
if args.cmd == "build":
|
|
155
|
+
return _run_build(args)
|
|
156
|
+
if args.cmd == "update":
|
|
157
|
+
return _run_update(args)
|
|
158
|
+
if args.cmd == "versions":
|
|
159
|
+
return _run_versions(args)
|
|
160
|
+
if args.cmd == "test":
|
|
161
|
+
return _run_test(args)
|
|
162
|
+
if args.cmd == "export":
|
|
163
|
+
return _run_export(args)
|
|
164
|
+
if args.cmd in ("look", "looks"):
|
|
165
|
+
return lookcli.run(args)
|
|
166
|
+
parser.error("a subcommand is required")
|
|
167
|
+
except (toolchain.ToolchainError, ValueError, OSError) as e:
|
|
168
|
+
print(f"tealui: {e}", file=sys.stderr)
|
|
169
|
+
return 2
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
if __name__ == "__main__":
|
|
173
|
+
raise SystemExit(main())
|
tealui/core.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import html
|
|
2
|
+
import re
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
class Raw(str):
|
|
6
|
+
"""Trusted markup, never escaped as child."""
|
|
7
|
+
|
|
8
|
+
_VOID = frozenset("area base br col embed hr img input link meta source track wbr".split())
|
|
9
|
+
|
|
10
|
+
def attr_name(key: str) -> str:
|
|
11
|
+
"""Convert Python kwarg to HTML/Alpine attribute name."""
|
|
12
|
+
if any(c in key for c in "@:.-"):
|
|
13
|
+
if not re.fullmatch(r"[A-Za-z_:@][A-Za-z0-9_:.@-]*", key):
|
|
14
|
+
raise ValueError(f"bad attribute name: {key!r}")
|
|
15
|
+
return key
|
|
16
|
+
if key == "cls":
|
|
17
|
+
key = "class"
|
|
18
|
+
key = key.rstrip("_")
|
|
19
|
+
base, *mods = key.split("__")
|
|
20
|
+
if base.startswith("on_"):
|
|
21
|
+
base = "@" + base[3:]
|
|
22
|
+
elif base.startswith("bind_"):
|
|
23
|
+
base = ":" + base[5:]
|
|
24
|
+
parts = [p.replace("_", "-") for p in [base] + mods]
|
|
25
|
+
res = ".".join(parts)
|
|
26
|
+
if not re.fullmatch(r"[A-Za-z_:@][A-Za-z0-9_:.@-]*", res):
|
|
27
|
+
raise ValueError(f"bad attribute name: {res!r}")
|
|
28
|
+
return res
|
|
29
|
+
|
|
30
|
+
def _freeze(x):
|
|
31
|
+
"""Turn one-shot iterators (generators, map, zip...) into tuples so an element renders the same every time."""
|
|
32
|
+
if isinstance(x, (str, bytes, El)) or not hasattr(x, "__next__"):
|
|
33
|
+
if isinstance(x, list):
|
|
34
|
+
return [_freeze(i) for i in x]
|
|
35
|
+
return x
|
|
36
|
+
return tuple(_freeze(i) for i in x)
|
|
37
|
+
|
|
38
|
+
def _flat(v):
|
|
39
|
+
for x in v:
|
|
40
|
+
if isinstance(x, (list, tuple)):
|
|
41
|
+
yield from _flat(x)
|
|
42
|
+
elif x is not None and x is not False and x != "":
|
|
43
|
+
yield str(x)
|
|
44
|
+
|
|
45
|
+
_URL_ATTRS = frozenset(("href", "src", "action", "formaction", "poster", "xlink:href", "data", "background"))
|
|
46
|
+
# SVG <set>/<animate> can write a URL into href through these attributes.
|
|
47
|
+
_ANIM_ATTRS = frozenset(("to", "from", "by", "values"))
|
|
48
|
+
|
|
49
|
+
def _bad_url(v: str) -> bool:
|
|
50
|
+
check = "".join(c for c in v if ord(c) > 32 and not c.isspace()).lower()
|
|
51
|
+
return check.startswith(("javascript:", "vbscript:")) or \
|
|
52
|
+
(check.startswith("data:") and not re.fullmatch(r"data:image/(png|jpe?g|gif|webp|avif)[;,].*", check))
|
|
53
|
+
|
|
54
|
+
def _url_safe(k: str, v: str) -> str:
|
|
55
|
+
"""Check URL safety for specific attributes."""
|
|
56
|
+
# bound attributes (bind_href) are dynamic and are not checked.
|
|
57
|
+
k = k.lower()
|
|
58
|
+
if k in _URL_ATTRS and _bad_url(v):
|
|
59
|
+
return "#"
|
|
60
|
+
if k in _ANIM_ATTRS and any(_bad_url(part) for part in v.split(";")):
|
|
61
|
+
return "#"
|
|
62
|
+
return v
|
|
63
|
+
|
|
64
|
+
class El:
|
|
65
|
+
"""An HTML element."""
|
|
66
|
+
def __init__(self, tag: str, *children: Any, **attrs: Any):
|
|
67
|
+
"""Initialize the element with a tag, children, and attributes."""
|
|
68
|
+
if not re.fullmatch(r"[A-Za-z][A-Za-z0-9-]*", tag):
|
|
69
|
+
raise ValueError(f"bad tag name: {tag!r}")
|
|
70
|
+
if tag.lower() in _VOID and children:
|
|
71
|
+
raise ValueError(f"void element {tag!r} cannot have children")
|
|
72
|
+
self.tag, self.children, self.attrs = tag, tuple(_freeze(c) for c in children), attrs
|
|
73
|
+
|
|
74
|
+
def __str__(self) -> str:
|
|
75
|
+
"""Render the element to HTML."""
|
|
76
|
+
is_void = self.tag.lower() in _VOID
|
|
77
|
+
# script/style children follow normal rules: str escaped, Raw not.
|
|
78
|
+
a = []
|
|
79
|
+
for k, v in self.attrs.items():
|
|
80
|
+
if v is False or v is None:
|
|
81
|
+
continue
|
|
82
|
+
name = attr_name(k)
|
|
83
|
+
if v is True:
|
|
84
|
+
a.append(f" {name}")
|
|
85
|
+
else:
|
|
86
|
+
if name == "class" and isinstance(v, (list, tuple)):
|
|
87
|
+
v = " ".join(_flat(v))
|
|
88
|
+
if not v:
|
|
89
|
+
continue
|
|
90
|
+
sv = html.escape(str(v), quote=True)
|
|
91
|
+
sv = _url_safe(name, sv)
|
|
92
|
+
a.append(f' {name}="{sv}"')
|
|
93
|
+
o = f"<{self.tag}{''.join(a)}>"
|
|
94
|
+
if is_void:
|
|
95
|
+
return o
|
|
96
|
+
c = "".join(render(x) for x in self.children)
|
|
97
|
+
return f"{o}{c}</{self.tag}>"
|
|
98
|
+
|
|
99
|
+
Tag = El
|
|
100
|
+
|
|
101
|
+
def render(x: Any) -> str:
|
|
102
|
+
"""Render any child value to HTML."""
|
|
103
|
+
if isinstance(x, Raw):
|
|
104
|
+
return x
|
|
105
|
+
if isinstance(x, str):
|
|
106
|
+
return html.escape(x, quote=True)
|
|
107
|
+
if isinstance(x, El):
|
|
108
|
+
return str(x)
|
|
109
|
+
if x is None or x is False:
|
|
110
|
+
return ""
|
|
111
|
+
if isinstance(x, (list, tuple)) or hasattr(x, "__next__"):
|
|
112
|
+
return "".join(render(i) for i in x)
|
|
113
|
+
return html.escape(str(x), quote=True)
|
tealui/devtools.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import importlib.util
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
import zipfile
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import tealui
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _project_root(start=None):
|
|
12
|
+
"""Walk up from start (or this file) to find the project root."""
|
|
13
|
+
p = Path(start) if start else Path(__file__).resolve().parent
|
|
14
|
+
for parent in [p] + list(p.parents):
|
|
15
|
+
if (parent / "pyproject.toml").is_file() and (parent / "tests").is_dir():
|
|
16
|
+
return parent
|
|
17
|
+
return None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _run_test(args):
|
|
21
|
+
root = _project_root()
|
|
22
|
+
if root is None:
|
|
23
|
+
print("tealui: tests are only available in a source checkout", file=sys.stderr)
|
|
24
|
+
return 2
|
|
25
|
+
res = subprocess.run([sys.executable, "-m", "unittest", "discover", "-s", "tests"], cwd=root)
|
|
26
|
+
if res.returncode != 0 or not getattr(args, "browser", False):
|
|
27
|
+
return res.returncode
|
|
28
|
+
compat = root / "tools" / "compat.py"
|
|
29
|
+
if not compat.is_file():
|
|
30
|
+
print("tealui: tools/compat.py not found", file=sys.stderr)
|
|
31
|
+
return 2
|
|
32
|
+
code = subprocess.run([sys.executable, str(compat)], cwd=root).returncode
|
|
33
|
+
for name in ("a11y_suite.py", "looks_suite.py"):
|
|
34
|
+
suite = root / "tools" / name
|
|
35
|
+
if code == 0 and suite.is_file():
|
|
36
|
+
code = subprocess.run([sys.executable, str(suite)], cwd=root).returncode
|
|
37
|
+
return code
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _wheel_problems(path):
|
|
41
|
+
problems = []
|
|
42
|
+
required = {
|
|
43
|
+
"tealui/fonts/inter.woff2", "tealui/fonts/serif.woff2",
|
|
44
|
+
"tealui/fonts/serif-italic.woff2", "tealui/fonts/mono.woff2",
|
|
45
|
+
"tealui/fonts/LICENSES.txt", "tealui/icons-LICENSE.txt", "tealui/py.typed"
|
|
46
|
+
}
|
|
47
|
+
with zipfile.ZipFile(path) as zf:
|
|
48
|
+
names = set(zf.namelist())
|
|
49
|
+
for req in required:
|
|
50
|
+
if req not in names:
|
|
51
|
+
problems.append(f"missing {req}")
|
|
52
|
+
for name in names:
|
|
53
|
+
if name.startswith("tests/"):
|
|
54
|
+
problems.append(f"unexpected {name}")
|
|
55
|
+
if "__pycache__" in name:
|
|
56
|
+
problems.append(f"unexpected {name}")
|
|
57
|
+
return problems
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _write_source_zip(root, outdir, version):
|
|
61
|
+
zip_path = Path(outdir) / f"tealui-{version}-src.zip"
|
|
62
|
+
prefix = f"tealui-{version}/"
|
|
63
|
+
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
64
|
+
for item in ["tealui", "pyproject.toml", "README.md"]:
|
|
65
|
+
p = root / item
|
|
66
|
+
if p.is_file():
|
|
67
|
+
zf.write(p, prefix + item)
|
|
68
|
+
elif p.is_dir():
|
|
69
|
+
for sub in p.rglob("*"):
|
|
70
|
+
if sub.is_file() and "__pycache__" not in sub.parts and not sub.name.endswith(".pyc"):
|
|
71
|
+
zf.write(sub, prefix + str(sub.relative_to(root)))
|
|
72
|
+
if (root / "LICENSE").is_file():
|
|
73
|
+
zf.write(root / "LICENSE", prefix + "LICENSE")
|
|
74
|
+
return zip_path
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _describe(paths):
|
|
78
|
+
lines = []
|
|
79
|
+
for p in sorted(paths):
|
|
80
|
+
size_kb = p.stat().st_size / 1024
|
|
81
|
+
h = hashlib.sha256(p.read_bytes()).hexdigest()[:12]
|
|
82
|
+
lines.append(f"{size_kb:.1f} KB {h} {p}")
|
|
83
|
+
return lines
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _run_export(args):
|
|
87
|
+
root = _project_root()
|
|
88
|
+
if root is None:
|
|
89
|
+
print("tealui: tests are only available in a source checkout", file=sys.stderr)
|
|
90
|
+
return 2
|
|
91
|
+
outdir = Path(args.outdir)
|
|
92
|
+
if not args.no_test and _run_test(type("A", (), {"browser": False})()) != 0:
|
|
93
|
+
print("tealui: tests failed, nothing exported", file=sys.stderr)
|
|
94
|
+
return 1
|
|
95
|
+
if importlib.util.find_spec("build") is None:
|
|
96
|
+
print("tealui: install the build tool first: pip install build", file=sys.stderr)
|
|
97
|
+
return 2
|
|
98
|
+
outdir.mkdir(parents=True, exist_ok=True)
|
|
99
|
+
cmd = [sys.executable, "-m", "build", "--outdir", str(outdir), str(root)]
|
|
100
|
+
if args.wheel_only:
|
|
101
|
+
cmd.insert(3, "--wheel")
|
|
102
|
+
if subprocess.run(cmd).returncode != 0:
|
|
103
|
+
print("tealui: build failed", file=sys.stderr)
|
|
104
|
+
return 1
|
|
105
|
+
return _finalize_export(outdir, root, args.wheel_only)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _finalize_export(outdir, root, wheel_only):
|
|
109
|
+
wheels = sorted(outdir.glob("*.whl"))
|
|
110
|
+
if not wheels:
|
|
111
|
+
print("tealui: build failed", file=sys.stderr)
|
|
112
|
+
return 1
|
|
113
|
+
problems = _wheel_problems(wheels[-1])
|
|
114
|
+
if problems:
|
|
115
|
+
for p in problems:
|
|
116
|
+
print(f"tealui: wheel problem: {p}", file=sys.stderr)
|
|
117
|
+
return 1
|
|
118
|
+
produced = [wheels[-1]]
|
|
119
|
+
if not wheel_only:
|
|
120
|
+
sdist = sorted(outdir.glob("*.tar.gz"))
|
|
121
|
+
if sdist:
|
|
122
|
+
produced.append(sdist[-1])
|
|
123
|
+
produced.append(_write_source_zip(root, outdir, tealui.__version__))
|
|
124
|
+
for line in _describe(produced):
|
|
125
|
+
print(line)
|
|
126
|
+
return 0
|