From e2f3aa631d95cef5cf4735a79cf265e8483ce50e Mon Sep 17 00:00:00 2001 From: rmorshea Date: Thu, 2 Sep 2021 23:45:51 -0700 Subject: [PATCH 1/6] add a template for react modules that export default --- src/idom/web/module.py | 33 ++++++++++++++++++++++--- src/idom/web/templates/react-default.js | 5 ++++ 2 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 src/idom/web/templates/react-default.js diff --git a/src/idom/web/module.py b/src/idom/web/module.py index 1d4f6b857..60da0210b 100644 --- a/src/idom/web/module.py +++ b/src/idom/web/module.py @@ -9,7 +9,7 @@ from dataclasses import dataclass from functools import partial from pathlib import Path -from typing import Any, List, NewType, Optional, Set, Tuple, Union, overload +from typing import Any, Dict, List, NewType, Optional, Set, Tuple, Union, overload from urllib.parse import urlparse from typing_extensions import Protocol @@ -99,10 +99,14 @@ def module_from_template( warning.cIt's best to author a module adhering to the :ref:`Custom Javascript Component` interface instead. + **Templates** + + - ``react``: for modules exporting React components + - ``react-default``: for React modules that use ``export default`` + Parameters: template: - The name of the framework template to use with the given ``package`` - (only ``react`` is supported at the moment). + The name of the framework template to use with the given ``package``. package: The name of a package to load. May include a file extension (defaults to ``.js`` if not given) @@ -137,7 +141,7 @@ def module_from_template( if not target_file.exists(): target_file.parent.mkdir(parents=True, exist_ok=True) target_file.write_text( - template_file.read_text().replace("$PACKAGE", package).replace("$CDN", cdn) + _resolve_template(template_file, {"$PACKAGE": package, "$CDN": cdn}) ) return WebModule( @@ -316,3 +320,24 @@ def _web_module_path(name: str, prefix: str = "") -> Path: directory /= prefix path = directory.joinpath(*name.split("/")) return path.with_suffix(path.suffix) + + +def _resolve_template(file: Path, substitutions: Dict[str, str]) -> str: + # NOTE: If this needs to be any more complex than it is, we should really + # reconsider this solution. Either use a real templating solution like Jinja + # or do something else entirely. + resolved_lines = [] + for line in file.read_text().splitlines(): + if line.startswith("$TEMPLATE:"): + relative_path = line.split(":", 1)[1].strip() + inner_template_file = file.parent.joinpath(*relative_path.split("/")) + resolved_lines.append(_resolve_template(inner_template_file, {})) + else: + resolved_lines.append(line) + + result = "\n".join(resolved_lines) + if substitutions: + for k, v in substitutions.items(): + result = result.replace(k, v) + + return result diff --git a/src/idom/web/templates/react-default.js b/src/idom/web/templates/react-default.js new file mode 100644 index 000000000..95f960cb1 --- /dev/null +++ b/src/idom/web/templates/react-default.js @@ -0,0 +1,5 @@ +// add the default export +export { default } from "$CDN/$PACKAGE"; + +// insert the normal react template +$TEMPLATE:react.js From 07dd0f48cc4b8f66c52bf52217d4ae42571cdbab Mon Sep 17 00:00:00 2001 From: rmorshea Date: Thu, 2 Sep 2021 23:52:21 -0700 Subject: [PATCH 2/6] add basic example with cytoscape --- docs/source/_static/custom.js | 296 +++++++++++++++--------------- docs/source/examples/cytoscape.py | 39 ++++ docs/source/index.rst | 13 +- 3 files changed, 196 insertions(+), 152 deletions(-) create mode 100644 docs/source/examples/cytoscape.py diff --git a/docs/source/_static/custom.js b/docs/source/_static/custom.js index 731d93d5e..d991871c8 100644 --- a/docs/source/_static/custom.js +++ b/docs/source/_static/custom.js @@ -620,147 +620,147 @@ function checkDCE() { var n=function(t,s,r,e){var u;s[0]=0;for(var h=1;h=5&&((e||!n&&5===r)&&(h.push(r,0,e,s),r=6),n&&(h.push(r,n,0,s),r=6)),e="";},a=0;a"===t?(r=1,e=""):e=t+e[0]:u?t===u?u="":e+=t:'"'===t||"'"===t?u=t:">"===t?(p(),r=1):r&&("="===t?(r=5,s=e,e=""):"/"===t&&(r<5||">"===n[a][l+1])?(p(),3===r&&(h=h[0]),r=h,(h=h[0]).push(2,0,r),r=0):" "===t||"\t"===t||"\n"===t||"\r"===t?(p(),r=2):e+=t),3===r&&"!--"===e&&(r=4,h=h[0]);}return p(),h}(s)),r),arguments,[])).length>1?r:r[0]} -/*! - * https://github.com/Starcounter-Jack/JSON-Patch - * (c) 2017 Joachim Wester - * MIT license - */ -var __extends = (undefined && undefined.__extends) || (function () { - var extendStatics = function (d, b) { - extendStatics = Object.setPrototypeOf || - ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || - function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; - return extendStatics(d, b); - }; - return function (d, b) { - extendStatics(d, b); - function __() { this.constructor = d; } - d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); - }; -})(); -var _hasOwnProperty = Object.prototype.hasOwnProperty; -function hasOwnProperty(obj, key) { - return _hasOwnProperty.call(obj, key); -} -function _objectKeys(obj) { - if (Array.isArray(obj)) { - var keys = new Array(obj.length); - for (var k = 0; k < keys.length; k++) { - keys[k] = "" + k; - } - return keys; - } - if (Object.keys) { - return Object.keys(obj); - } - var keys = []; - for (var i in obj) { - if (hasOwnProperty(obj, i)) { - keys.push(i); - } - } - return keys; -} -/** -* Deeply clone the object. -* https://jsperf.com/deep-copy-vs-json-stringify-json-parse/25 (recursiveDeepCopy) -* @param {any} obj value to clone -* @return {any} cloned obj -*/ -function _deepClone(obj) { - switch (typeof obj) { - case "object": - return JSON.parse(JSON.stringify(obj)); //Faster than ES5 clone - http://jsperf.com/deep-cloning-of-objects/5 - case "undefined": - return null; //this is how JSON.stringify behaves for array items - default: - return obj; //no need to clone primitives - } -} -//3x faster than cached /^\d+$/.test(str) -function isInteger(str) { - var i = 0; - var len = str.length; - var charCode; - while (i < len) { - charCode = str.charCodeAt(i); - if (charCode >= 48 && charCode <= 57) { - i++; - continue; - } - return false; - } - return true; -} -/** -* Escapes a json pointer path -* @param path The raw pointer -* @return the Escaped path -*/ -function escapePathComponent(path) { - if (path.indexOf('/') === -1 && path.indexOf('~') === -1) - return path; - return path.replace(/~/g, '~0').replace(/\//g, '~1'); -} -/** - * Unescapes a json pointer path - * @param path The escaped pointer - * @return The unescaped path - */ -function unescapePathComponent(path) { - return path.replace(/~1/g, '/').replace(/~0/g, '~'); -} -/** -* Recursively checks whether an object has any undefined values inside. -*/ -function hasUndefined(obj) { - if (obj === undefined) { - return true; - } - if (obj) { - if (Array.isArray(obj)) { - for (var i = 0, len = obj.length; i < len; i++) { - if (hasUndefined(obj[i])) { - return true; - } - } - } - else if (typeof obj === "object") { - var objKeys = _objectKeys(obj); - var objKeysLength = objKeys.length; - for (var i = 0; i < objKeysLength; i++) { - if (hasUndefined(obj[objKeys[i]])) { - return true; - } - } - } - } - return false; -} -function patchErrorMessageFormatter(message, args) { - var messageParts = [message]; - for (var key in args) { - var value = typeof args[key] === 'object' ? JSON.stringify(args[key], null, 2) : args[key]; // pretty print - if (typeof value !== 'undefined') { - messageParts.push(key + ": " + value); - } - } - return messageParts.join('\n'); -} -var PatchError = /** @class */ (function (_super) { - __extends(PatchError, _super); - function PatchError(message, name, index, operation, tree) { - var _newTarget = this.constructor; - var _this = _super.call(this, patchErrorMessageFormatter(message, { name: name, index: index, operation: operation, tree: tree })) || this; - _this.name = name; - _this.index = index; - _this.operation = operation; - _this.tree = tree; - Object.setPrototypeOf(_this, _newTarget.prototype); // restore prototype chain, see https://stackoverflow.com/a/48342359 - _this.message = patchErrorMessageFormatter(message, { name: name, index: index, operation: operation, tree: tree }); - return _this; - } - return PatchError; +/*! + * https://github.com/Starcounter-Jack/JSON-Patch + * (c) 2017 Joachim Wester + * MIT license + */ +var __extends = (undefined && undefined.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +var _hasOwnProperty = Object.prototype.hasOwnProperty; +function hasOwnProperty(obj, key) { + return _hasOwnProperty.call(obj, key); +} +function _objectKeys(obj) { + if (Array.isArray(obj)) { + var keys = new Array(obj.length); + for (var k = 0; k < keys.length; k++) { + keys[k] = "" + k; + } + return keys; + } + if (Object.keys) { + return Object.keys(obj); + } + var keys = []; + for (var i in obj) { + if (hasOwnProperty(obj, i)) { + keys.push(i); + } + } + return keys; +} +/** +* Deeply clone the object. +* https://jsperf.com/deep-copy-vs-json-stringify-json-parse/25 (recursiveDeepCopy) +* @param {any} obj value to clone +* @return {any} cloned obj +*/ +function _deepClone(obj) { + switch (typeof obj) { + case "object": + return JSON.parse(JSON.stringify(obj)); //Faster than ES5 clone - http://jsperf.com/deep-cloning-of-objects/5 + case "undefined": + return null; //this is how JSON.stringify behaves for array items + default: + return obj; //no need to clone primitives + } +} +//3x faster than cached /^\d+$/.test(str) +function isInteger(str) { + var i = 0; + var len = str.length; + var charCode; + while (i < len) { + charCode = str.charCodeAt(i); + if (charCode >= 48 && charCode <= 57) { + i++; + continue; + } + return false; + } + return true; +} +/** +* Escapes a json pointer path +* @param path The raw pointer +* @return the Escaped path +*/ +function escapePathComponent(path) { + if (path.indexOf('/') === -1 && path.indexOf('~') === -1) + return path; + return path.replace(/~/g, '~0').replace(/\//g, '~1'); +} +/** + * Unescapes a json pointer path + * @param path The escaped pointer + * @return The unescaped path + */ +function unescapePathComponent(path) { + return path.replace(/~1/g, '/').replace(/~0/g, '~'); +} +/** +* Recursively checks whether an object has any undefined values inside. +*/ +function hasUndefined(obj) { + if (obj === undefined) { + return true; + } + if (obj) { + if (Array.isArray(obj)) { + for (var i = 0, len = obj.length; i < len; i++) { + if (hasUndefined(obj[i])) { + return true; + } + } + } + else if (typeof obj === "object") { + var objKeys = _objectKeys(obj); + var objKeysLength = objKeys.length; + for (var i = 0; i < objKeysLength; i++) { + if (hasUndefined(obj[objKeys[i]])) { + return true; + } + } + } + } + return false; +} +function patchErrorMessageFormatter(message, args) { + var messageParts = [message]; + for (var key in args) { + var value = typeof args[key] === 'object' ? JSON.stringify(args[key], null, 2) : args[key]; // pretty print + if (typeof value !== 'undefined') { + messageParts.push(key + ": " + value); + } + } + return messageParts.join('\n'); +} +var PatchError = /** @class */ (function (_super) { + __extends(PatchError, _super); + function PatchError(message, name, index, operation, tree) { + var _newTarget = this.constructor; + var _this = _super.call(this, patchErrorMessageFormatter(message, { name: name, index: index, operation: operation, tree: tree })) || this; + _this.name = name; + _this.index = index; + _this.operation = operation; + _this.tree = tree; + Object.setPrototypeOf(_this, _newTarget.prototype); // restore prototype chain, see https://stackoverflow.com/a/48342359 + _this.message = patchErrorMessageFormatter(message, { name: name, index: index, operation: operation, tree: tree }); + return _this; + } + return PatchError; }(Error)); var JsonPatchError = PatchError; @@ -1381,11 +1381,11 @@ var duplex = /*#__PURE__*/Object.freeze({ compare: compare }); -var jsonpatch = Object.assign({}, core, duplex, { - JsonPatchError: PatchError, - deepClone: _deepClone, - escapePathComponent, - unescapePathComponent +var jsonpatch = Object.assign({}, core, duplex, { + JsonPatchError: PatchError, + deepClone: _deepClone, + escapePathComponent, + unescapePathComponent }); function useJsonPatchCallback(initial) { @@ -1398,7 +1398,9 @@ function useJsonPatchCallback(initial) { // We CANNOT mutate the part of the document because React checks some // attributes of the model (e.g. model.attributes.style is checked for // identity). - doc.current = applyNonMutativePatch(doc.current, patch); + doc.current = applyNonMutativePatch( + doc.current, + patch); } else { // We CAN mutate the document here though because we know that nothing above // The patch `path` is changing. Thus, maintaining the identity for that section diff --git a/docs/source/examples/cytoscape.py b/docs/source/examples/cytoscape.py new file mode 100644 index 000000000..c4a606e72 --- /dev/null +++ b/docs/source/examples/cytoscape.py @@ -0,0 +1,39 @@ +import idom + + +react_cytoscapejs = idom.web.module_from_template( + # we need to use this template because react-cytoscapejs uses a default export + "react-default", + "react-cytoscapejs", + fallback="βŒ›", +) +Cytoscape = idom.web.export(react_cytoscapejs, "default") + + +@idom.component +def CytoscapeGraph(): + return Cytoscape( + { + "style": {"width": "100%", "height": "200px"}, + "elements": [ + { + "data": {"id": "one", "label": "Node 1"}, + "position": {"x": 100, "y": 100}, + }, + { + "data": {"id": "two", "label": "Node 2"}, + "position": {"x": 200, "y": 100}, + }, + { + "data": { + "source": "one", + "target": "two", + "label": "Edge from Node1 to Node2", + } + }, + ], + } + ) + + +idom.run(CytoscapeGraph) diff --git a/docs/source/index.rst b/docs/source/index.rst index f095d4d14..3ab4e2177 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -75,17 +75,17 @@ A package for building responsive user interfaces in pure Python. .. grid-item-card:: - .. interactive-widget:: slideshow + .. interactive-widget:: cytoscape :no-activate-button: .. grid-item-card:: - .. interactive-widget:: audio_player + .. interactive-widget:: slideshow :no-activate-button: .. grid-item-card:: - .. interactive-widget:: todo + .. interactive-widget:: audio_player :no-activate-button: .. grid-item:: @@ -98,8 +98,6 @@ A package for building responsive user interfaces in pure Python. .. interactive-widget:: simple_dashboard :no-activate-button: - - .. grid-item-card:: .. interactive-widget:: matplotlib_plot @@ -109,3 +107,8 @@ A package for building responsive user interfaces in pure Python. .. interactive-widget:: material_ui_button_on_click :no-activate-button: + + .. grid-item-card:: + + .. interactive-widget:: todo + :no-activate-button: From 51651c97174e57ddddef03f31f4631d0035d6b44 Mon Sep 17 00:00:00 2001 From: rmorshea Date: Thu, 2 Sep 2021 23:52:33 -0700 Subject: [PATCH 3/6] run format on js --- src/client/package-lock.json | 8 ++++++++ src/client/packages/idom-client-react/src/json-patch.js | 8 +++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/client/package-lock.json b/src/client/package-lock.json index 628ee76fd..10c104d8e 100644 --- a/src/client/package-lock.json +++ b/src/client/package-lock.json @@ -2645,7 +2645,11 @@ "dev": true }, "packages/idom-app-react": { +<<<<<<< HEAD "version": "0.33.2", +======= + "version": "0.33.1", +>>>>>>> run format on js "license": "MIT", "dependencies": { "idom-client-react": "file:packages/idom-client-react", @@ -2662,7 +2666,11 @@ }, "packages/idom-app-react/packages/idom-client-react": {}, "packages/idom-client-react": { +<<<<<<< HEAD "version": "0.33.2", +======= + "version": "0.33.1", +>>>>>>> run format on js "license": "MIT", "dependencies": { "fast-json-patch": "^3.0.0-1", diff --git a/src/client/packages/idom-client-react/src/json-patch.js b/src/client/packages/idom-client-react/src/json-patch.js index d79a80caa..5323f11a9 100644 --- a/src/client/packages/idom-client-react/src/json-patch.js +++ b/src/client/packages/idom-client-react/src/json-patch.js @@ -11,7 +11,13 @@ export function useJsonPatchCallback(initial) { // We CANNOT mutate the part of the document because React checks some // attributes of the model (e.g. model.attributes.style is checked for // identity). - doc.current = applyNonMutativePatch(doc.current, patch, false, false, true); + doc.current = applyNonMutativePatch( + doc.current, + patch, + false, + false, + true + ); } else { // We CAN mutate the document here though because we know that nothing above // The patch `path` is changing. Thus, maintaining the identity for that section From ec218f636a560ab0019cb353976397453092dc4c Mon Sep 17 00:00:00 2001 From: rmorshea Date: Wed, 29 Sep 2021 21:59:16 -0700 Subject: [PATCH 4/6] add exports_default parameter --- docs/source/examples/cytoscape.py | 3 +- src/client/package-lock.json | 8 --- src/idom/web/module.py | 12 ++++- .../{react-default.js => react.default.js} | 0 src/idom/web/utils.py | 49 +++++++++++++++---- .../js_fixtures/export-resolution/one.js | 2 + tests/test_web/test_utils.py | 14 +++++- 7 files changed, 65 insertions(+), 23 deletions(-) rename src/idom/web/templates/{react-default.js => react.default.js} (100%) diff --git a/docs/source/examples/cytoscape.py b/docs/source/examples/cytoscape.py index c4a606e72..34f439f69 100644 --- a/docs/source/examples/cytoscape.py +++ b/docs/source/examples/cytoscape.py @@ -3,8 +3,9 @@ react_cytoscapejs = idom.web.module_from_template( # we need to use this template because react-cytoscapejs uses a default export - "react-default", + "react", "react-cytoscapejs", + exports_default=True, fallback="βŒ›", ) Cytoscape = idom.web.export(react_cytoscapejs, "default") diff --git a/src/client/package-lock.json b/src/client/package-lock.json index 10c104d8e..628ee76fd 100644 --- a/src/client/package-lock.json +++ b/src/client/package-lock.json @@ -2645,11 +2645,7 @@ "dev": true }, "packages/idom-app-react": { -<<<<<<< HEAD "version": "0.33.2", -======= - "version": "0.33.1", ->>>>>>> run format on js "license": "MIT", "dependencies": { "idom-client-react": "file:packages/idom-client-react", @@ -2666,11 +2662,7 @@ }, "packages/idom-app-react/packages/idom-client-react": {}, "packages/idom-client-react": { -<<<<<<< HEAD "version": "0.33.2", -======= - "version": "0.33.1", ->>>>>>> run format on js "license": "MIT", "dependencies": { "fast-json-patch": "^3.0.0-1", diff --git a/src/idom/web/module.py b/src/idom/web/module.py index 60da0210b..dd51b957e 100644 --- a/src/idom/web/module.py +++ b/src/idom/web/module.py @@ -83,6 +83,7 @@ def module_from_template( package: str, cdn: str = "https://esm.sh", fallback: Optional[Any] = None, + exports_default: bool = False, resolve_exports: bool = IDOM_DEBUG_MODE.current, resolve_exports_depth: int = 5, unmount_before_update: bool = False, @@ -114,6 +115,8 @@ def module_from_template( Where the package should be loaded from. The CDN must distribute ESM modules fallback: What to temporarilly display while the module is being loaded. + exports_default: + Whether the module has a default export. resolve_imports: Whether to try and find all the named exports of this module. resolve_exports_depth: @@ -132,7 +135,12 @@ def module_from_template( # downstream code assumes no trailing slash cdn = cdn.rstrip("/") - template_file_name = f"{template}{module_name_suffix(package_name)}" + template_file_name = ( + template + + (".default" if exports_default else "") + + module_name_suffix(package_name) + ) + template_file = Path(__file__).parent / "templates" / template_file_name if not template_file.exists(): raise ValueError(f"No template for {template_file_name!r} exists") @@ -150,7 +158,7 @@ def module_from_template( default_fallback=fallback, file=target_file, export_names=( - resolve_module_exports_from_url(f"{cdn}/{package}", resolve_exports_depth) + resolve_module_exports_from_file(target_file, resolve_exports_depth) if resolve_exports else None ), diff --git a/src/idom/web/templates/react-default.js b/src/idom/web/templates/react.default.js similarity index 100% rename from src/idom/web/templates/react-default.js rename to src/idom/web/templates/react.default.js diff --git a/src/idom/web/utils.py b/src/idom/web/utils.py index 5c8ae6718..2916cbb98 100644 --- a/src/idom/web/utils.py +++ b/src/idom/web/utils.py @@ -18,7 +18,11 @@ def module_name_suffix(name: str) -> str: return PurePosixPath(tail or head).suffix or ".js" -def resolve_module_exports_from_file(file: Path, max_depth: int) -> Set[str]: +def resolve_module_exports_from_file( + file: Path, + max_depth: int, + is_re_export: bool = False, +) -> Set[str]: if max_depth == 0: logger.warning(f"Did not resolve all exports for {file} - max depth reached") return set() @@ -26,19 +30,29 @@ def resolve_module_exports_from_file(file: Path, max_depth: int) -> Set[str]: logger.warning(f"Did not resolve exports for unknown file {file}") return set() - export_names, references = resolve_module_exports_from_source(file.read_text()) + export_names, references = resolve_module_exports_from_source( + file.read_text(), exclude_default=is_re_export + ) for ref in references: if urlparse(ref).scheme: # is an absolute URL - export_names.update(resolve_module_exports_from_url(ref, max_depth - 1)) + export_names.update( + resolve_module_exports_from_url(ref, max_depth - 1, is_re_export=True) + ) else: path = file.parent.joinpath(*ref.split("/")) - export_names.update(resolve_module_exports_from_file(path, max_depth - 1)) + export_names.update( + resolve_module_exports_from_file(path, max_depth - 1, is_re_export=True) + ) return export_names -def resolve_module_exports_from_url(url: str, max_depth: int) -> Set[str]: +def resolve_module_exports_from_url( + url: str, + max_depth: int, + is_re_export: bool = False, +) -> Set[str]: if max_depth == 0: logger.warning(f"Did not resolve all exports for {url} - max depth reached") return set() @@ -50,16 +64,22 @@ def resolve_module_exports_from_url(url: str, max_depth: int) -> Set[str]: logger.warning("Did not resolve exports for url " + url + reason) return set() - export_names, references = resolve_module_exports_from_source(text) + export_names, references = resolve_module_exports_from_source( + text, exclude_default=is_re_export + ) for ref in references: url = _resolve_relative_url(url, ref) - export_names.update(resolve_module_exports_from_url(url, max_depth - 1)) + export_names.update( + resolve_module_exports_from_url(url, max_depth - 1, is_re_export=True) + ) return export_names -def resolve_module_exports_from_source(content: str) -> Tuple[Set[str], Set[str]]: +def resolve_module_exports_from_source( + content: str, exclude_default: bool +) -> Tuple[Set[str], Set[str]]: names: Set[str] = set() references: Set[str] = set() @@ -69,7 +89,9 @@ def resolve_module_exports_from_source(content: str) -> Tuple[Set[str], Set[str] # Exporting functions and classes names.update(_JS_FUNC_OR_CLS_EXPORT_PATTERN.findall(content)) + print(content) for export in _JS_GENERAL_EXPORT_PATTERN.findall(content): + print(export) export = export.rstrip(";").strip() # Exporting individual features if export.startswith("let "): @@ -100,7 +122,14 @@ def resolve_module_exports_from_source(content: str) -> Tuple[Set[str], Set[str] ) elif not (export.startswith("function ") or export.startswith("class ")): logger.warning(f"Unknown export type {export!r}") - return {n.strip() for n in names}, {r.strip() for r in references} + + names = {n.strip() for n in names} + references = {r.strip() for r in references} + + if exclude_default and "default" in names: + names.remove("default") + + return names, references def _resolve_relative_url(base_url: str, rel_url: str) -> str: @@ -126,5 +155,5 @@ def _resolve_relative_url(base_url: str, rel_url: str) -> str: r";?\s*export\s+(?:function|class)\s+([a-zA-Z_$][0-9a-zA-Z_$]*)" ) _JS_GENERAL_EXPORT_PATTERN = re.compile( - r";?\s*export(?=\s+|{)(.*?)(?:;|$)", re.MULTILINE + r"(?:^|;)\s*export(?=\s+|{)(.*?)(?=;|$)", re.MULTILINE ) diff --git a/tests/test_web/js_fixtures/export-resolution/one.js b/tests/test_web/js_fixtures/export-resolution/one.js index a0355241f..1d404b1cc 100644 --- a/tests/test_web/js_fixtures/export-resolution/one.js +++ b/tests/test_web/js_fixtures/export-resolution/one.js @@ -1,3 +1,5 @@ export {one as One}; // use ../ just to check that it works export * from "../export-resolution/two.js"; +// this default should not be exported by the * re-export in index.js +export default 0; diff --git a/tests/test_web/test_utils.py b/tests/test_web/test_utils.py index 3ed31b39e..149c6eb8f 100644 --- a/tests/test_web/test_utils.py +++ b/tests/test_web/test_utils.py @@ -127,13 +127,15 @@ def test_resolve_module_exports_from_url_log_on_bad_response(caplog): ], ) def test_resolve_module_default_exports_from_source(text): - names, references = resolve_module_exports_from_source(text) + names, references = resolve_module_exports_from_source(text, exclude_default=False) assert names == {"default"} and not references def test_resolve_module_exports_from_source(): fixture_file = JS_FIXTURES_DIR / "exports-syntax.js" - names, references = resolve_module_exports_from_source(fixture_file.read_text()) + names, references = resolve_module_exports_from_source( + fixture_file.read_text(), exclude_default=False + ) assert ( names == ( @@ -145,3 +147,11 @@ def test_resolve_module_exports_from_source(): ) and references == {"https://source1.com", "https://source2.com"} ) + + +def test_log_on_unknown_export_type(caplog): + assert resolve_module_exports_from_source( + "export something unknown;", exclude_default=False + ) == (set(), set()) + assert len(caplog.records) == 1 + assert caplog.records[0].message.startswith("Unknown export type ") From 1106dfd4a7047773ea55a5df913429ad97b72e6e Mon Sep 17 00:00:00 2001 From: rmorshea Date: Thu, 30 Sep 2021 00:28:06 -0700 Subject: [PATCH 5/6] update examples in docs --- docs/main.py | 22 +- docs/source/_static/custom.js | 297 +++++++++++++------------- docs/source/examples.rst | 14 +- docs/source/examples/cytoscape.py | 40 ---- docs/source/examples/network_graph.py | 43 ++++ docs/source/index.rst | 4 +- 6 files changed, 212 insertions(+), 208 deletions(-) delete mode 100644 docs/source/examples/cytoscape.py create mode 100644 docs/source/examples/network_graph.py diff --git a/docs/main.py b/docs/main.py index 20dfd7014..7a680b39c 100644 --- a/docs/main.py +++ b/docs/main.py @@ -46,18 +46,16 @@ def make_component(): # Modify the run function so when we exec the file # instead of running a server we mount the view. idom.run = partial(mount.add, file.stem) - - with file.open() as f: - try: - exec( - f.read(), - { - "__file__": str(file), - "__name__": f"__main__.examples.{file.stem}", - }, - ) - except Exception as error: - raise RuntimeError(f"Failed to execute {file}") from error + try: + exec( + file.read_text(), + { + "__file__": str(file.absolute()), + "__name__": f"__main__.examples.{file.stem}", + }, + ) + except Exception as error: + raise RuntimeError(f"Failed to execute {file}") from error finally: idom.run = original_run diff --git a/docs/source/_static/custom.js b/docs/source/_static/custom.js index d991871c8..b2206d073 100644 --- a/docs/source/_static/custom.js +++ b/docs/source/_static/custom.js @@ -620,147 +620,147 @@ function checkDCE() { var n=function(t,s,r,e){var u;s[0]=0;for(var h=1;h=5&&((e||!n&&5===r)&&(h.push(r,0,e,s),r=6),n&&(h.push(r,n,0,s),r=6)),e="";},a=0;a"===t?(r=1,e=""):e=t+e[0]:u?t===u?u="":e+=t:'"'===t||"'"===t?u=t:">"===t?(p(),r=1):r&&("="===t?(r=5,s=e,e=""):"/"===t&&(r<5||">"===n[a][l+1])?(p(),3===r&&(h=h[0]),r=h,(h=h[0]).push(2,0,r),r=0):" "===t||"\t"===t||"\n"===t||"\r"===t?(p(),r=2):e+=t),3===r&&"!--"===e&&(r=4,h=h[0]);}return p(),h}(s)),r),arguments,[])).length>1?r:r[0]} -/*! - * https://github.com/Starcounter-Jack/JSON-Patch - * (c) 2017 Joachim Wester - * MIT license - */ -var __extends = (undefined && undefined.__extends) || (function () { - var extendStatics = function (d, b) { - extendStatics = Object.setPrototypeOf || - ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || - function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; - return extendStatics(d, b); - }; - return function (d, b) { - extendStatics(d, b); - function __() { this.constructor = d; } - d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); - }; -})(); -var _hasOwnProperty = Object.prototype.hasOwnProperty; -function hasOwnProperty(obj, key) { - return _hasOwnProperty.call(obj, key); -} -function _objectKeys(obj) { - if (Array.isArray(obj)) { - var keys = new Array(obj.length); - for (var k = 0; k < keys.length; k++) { - keys[k] = "" + k; - } - return keys; - } - if (Object.keys) { - return Object.keys(obj); - } - var keys = []; - for (var i in obj) { - if (hasOwnProperty(obj, i)) { - keys.push(i); - } - } - return keys; -} -/** -* Deeply clone the object. -* https://jsperf.com/deep-copy-vs-json-stringify-json-parse/25 (recursiveDeepCopy) -* @param {any} obj value to clone -* @return {any} cloned obj -*/ -function _deepClone(obj) { - switch (typeof obj) { - case "object": - return JSON.parse(JSON.stringify(obj)); //Faster than ES5 clone - http://jsperf.com/deep-cloning-of-objects/5 - case "undefined": - return null; //this is how JSON.stringify behaves for array items - default: - return obj; //no need to clone primitives - } -} -//3x faster than cached /^\d+$/.test(str) -function isInteger(str) { - var i = 0; - var len = str.length; - var charCode; - while (i < len) { - charCode = str.charCodeAt(i); - if (charCode >= 48 && charCode <= 57) { - i++; - continue; - } - return false; - } - return true; -} -/** -* Escapes a json pointer path -* @param path The raw pointer -* @return the Escaped path -*/ -function escapePathComponent(path) { - if (path.indexOf('/') === -1 && path.indexOf('~') === -1) - return path; - return path.replace(/~/g, '~0').replace(/\//g, '~1'); -} -/** - * Unescapes a json pointer path - * @param path The escaped pointer - * @return The unescaped path - */ -function unescapePathComponent(path) { - return path.replace(/~1/g, '/').replace(/~0/g, '~'); -} -/** -* Recursively checks whether an object has any undefined values inside. -*/ -function hasUndefined(obj) { - if (obj === undefined) { - return true; - } - if (obj) { - if (Array.isArray(obj)) { - for (var i = 0, len = obj.length; i < len; i++) { - if (hasUndefined(obj[i])) { - return true; - } - } - } - else if (typeof obj === "object") { - var objKeys = _objectKeys(obj); - var objKeysLength = objKeys.length; - for (var i = 0; i < objKeysLength; i++) { - if (hasUndefined(obj[objKeys[i]])) { - return true; - } - } - } - } - return false; -} -function patchErrorMessageFormatter(message, args) { - var messageParts = [message]; - for (var key in args) { - var value = typeof args[key] === 'object' ? JSON.stringify(args[key], null, 2) : args[key]; // pretty print - if (typeof value !== 'undefined') { - messageParts.push(key + ": " + value); - } - } - return messageParts.join('\n'); -} -var PatchError = /** @class */ (function (_super) { - __extends(PatchError, _super); - function PatchError(message, name, index, operation, tree) { - var _newTarget = this.constructor; - var _this = _super.call(this, patchErrorMessageFormatter(message, { name: name, index: index, operation: operation, tree: tree })) || this; - _this.name = name; - _this.index = index; - _this.operation = operation; - _this.tree = tree; - Object.setPrototypeOf(_this, _newTarget.prototype); // restore prototype chain, see https://stackoverflow.com/a/48342359 - _this.message = patchErrorMessageFormatter(message, { name: name, index: index, operation: operation, tree: tree }); - return _this; - } - return PatchError; +/*! + * https://github.com/Starcounter-Jack/JSON-Patch + * (c) 2017 Joachim Wester + * MIT license + */ +var __extends = (undefined && undefined.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +var _hasOwnProperty = Object.prototype.hasOwnProperty; +function hasOwnProperty(obj, key) { + return _hasOwnProperty.call(obj, key); +} +function _objectKeys(obj) { + if (Array.isArray(obj)) { + var keys = new Array(obj.length); + for (var k = 0; k < keys.length; k++) { + keys[k] = "" + k; + } + return keys; + } + if (Object.keys) { + return Object.keys(obj); + } + var keys = []; + for (var i in obj) { + if (hasOwnProperty(obj, i)) { + keys.push(i); + } + } + return keys; +} +/** +* Deeply clone the object. +* https://jsperf.com/deep-copy-vs-json-stringify-json-parse/25 (recursiveDeepCopy) +* @param {any} obj value to clone +* @return {any} cloned obj +*/ +function _deepClone(obj) { + switch (typeof obj) { + case "object": + return JSON.parse(JSON.stringify(obj)); //Faster than ES5 clone - http://jsperf.com/deep-cloning-of-objects/5 + case "undefined": + return null; //this is how JSON.stringify behaves for array items + default: + return obj; //no need to clone primitives + } +} +//3x faster than cached /^\d+$/.test(str) +function isInteger(str) { + var i = 0; + var len = str.length; + var charCode; + while (i < len) { + charCode = str.charCodeAt(i); + if (charCode >= 48 && charCode <= 57) { + i++; + continue; + } + return false; + } + return true; +} +/** +* Escapes a json pointer path +* @param path The raw pointer +* @return the Escaped path +*/ +function escapePathComponent(path) { + if (path.indexOf('/') === -1 && path.indexOf('~') === -1) + return path; + return path.replace(/~/g, '~0').replace(/\//g, '~1'); +} +/** + * Unescapes a json pointer path + * @param path The escaped pointer + * @return The unescaped path + */ +function unescapePathComponent(path) { + return path.replace(/~1/g, '/').replace(/~0/g, '~'); +} +/** +* Recursively checks whether an object has any undefined values inside. +*/ +function hasUndefined(obj) { + if (obj === undefined) { + return true; + } + if (obj) { + if (Array.isArray(obj)) { + for (var i = 0, len = obj.length; i < len; i++) { + if (hasUndefined(obj[i])) { + return true; + } + } + } + else if (typeof obj === "object") { + var objKeys = _objectKeys(obj); + var objKeysLength = objKeys.length; + for (var i = 0; i < objKeysLength; i++) { + if (hasUndefined(obj[objKeys[i]])) { + return true; + } + } + } + } + return false; +} +function patchErrorMessageFormatter(message, args) { + var messageParts = [message]; + for (var key in args) { + var value = typeof args[key] === 'object' ? JSON.stringify(args[key], null, 2) : args[key]; // pretty print + if (typeof value !== 'undefined') { + messageParts.push(key + ": " + value); + } + } + return messageParts.join('\n'); +} +var PatchError = /** @class */ (function (_super) { + __extends(PatchError, _super); + function PatchError(message, name, index, operation, tree) { + var _newTarget = this.constructor; + var _this = _super.call(this, patchErrorMessageFormatter(message, { name: name, index: index, operation: operation, tree: tree })) || this; + _this.name = name; + _this.index = index; + _this.operation = operation; + _this.tree = tree; + Object.setPrototypeOf(_this, _newTarget.prototype); // restore prototype chain, see https://stackoverflow.com/a/48342359 + _this.message = patchErrorMessageFormatter(message, { name: name, index: index, operation: operation, tree: tree }); + return _this; + } + return PatchError; }(Error)); var JsonPatchError = PatchError; @@ -1381,11 +1381,11 @@ var duplex = /*#__PURE__*/Object.freeze({ compare: compare }); -var jsonpatch = Object.assign({}, core, duplex, { - JsonPatchError: PatchError, - deepClone: _deepClone, - escapePathComponent, - unescapePathComponent +var jsonpatch = Object.assign({}, core, duplex, { + JsonPatchError: PatchError, + deepClone: _deepClone, + escapePathComponent, + unescapePathComponent }); function useJsonPatchCallback(initial) { @@ -1967,11 +1967,6 @@ function _nextReconnectTimeout(maxReconnectTimeout, mountState) { Math.floor(Math.random() * mountState.reconnectTimeoutRange) || 1; mountState.reconnectTimeoutRange = (mountState.reconnectTimeoutRange + 5) % maxReconnectTimeout; - if (mountState.reconnectAttempts === 4) { - window.alert( - "Server connection was lost. Attempts to reconnect are being made in the background." - ); - } return timeout; } diff --git a/docs/source/examples.rst b/docs/source/examples.rst index 216902bcc..5e0b5cebf 100644 --- a/docs/source/examples.rst +++ b/docs/source/examples.rst @@ -62,15 +62,15 @@ you're experimenting: Define Javascript Modules ------------------------- -Click the bars to trigger an event πŸ‘‡ +Shows a very simple chart implemented in vanilla Javascript: .. example:: super_simple_chart -Material UI Slider +Material UI Button ------------------ -Move the slider and see the event information update πŸ‘‡ +Click the button to change the indicator πŸ‘‡ .. example:: material_ui_switch @@ -83,6 +83,14 @@ Click the map to create pinned location πŸ“: .. example:: pigeon_maps +Cytoscape Notework Graph +------------------------ + +You can move the nodes in the graph πŸ•ΈοΈ: + +.. example:: network_graph + + .. Links .. ===== diff --git a/docs/source/examples/cytoscape.py b/docs/source/examples/cytoscape.py deleted file mode 100644 index 34f439f69..000000000 --- a/docs/source/examples/cytoscape.py +++ /dev/null @@ -1,40 +0,0 @@ -import idom - - -react_cytoscapejs = idom.web.module_from_template( - # we need to use this template because react-cytoscapejs uses a default export - "react", - "react-cytoscapejs", - exports_default=True, - fallback="βŒ›", -) -Cytoscape = idom.web.export(react_cytoscapejs, "default") - - -@idom.component -def CytoscapeGraph(): - return Cytoscape( - { - "style": {"width": "100%", "height": "200px"}, - "elements": [ - { - "data": {"id": "one", "label": "Node 1"}, - "position": {"x": 100, "y": 100}, - }, - { - "data": {"id": "two", "label": "Node 2"}, - "position": {"x": 200, "y": 100}, - }, - { - "data": { - "source": "one", - "target": "two", - "label": "Edge from Node1 to Node2", - } - }, - ], - } - ) - - -idom.run(CytoscapeGraph) diff --git a/docs/source/examples/network_graph.py b/docs/source/examples/network_graph.py new file mode 100644 index 000000000..3dfb9ae87 --- /dev/null +++ b/docs/source/examples/network_graph.py @@ -0,0 +1,43 @@ +import random + +import idom + + +react_cytoscapejs = idom.web.module_from_template( + # we need to use this template because react-cytoscapejs uses a default export + "react", + "react-cytoscapejs", + exports_default=True, + fallback="βŒ›", +) +Cytoscape = idom.web.export(react_cytoscapejs, "default") + + +@idom.component +def RandomNetworkGraph(): + return Cytoscape( + { + "style": {"width": "100%", "height": "200px"}, + "elements": random_network(20), + "layout": {"name": "cose"}, + } + ) + + +def random_network(number_of_nodes): + conns = [] + nodes = [{"data": {"id": 0, "label": 0}}] + + for src_node_id in range(1, number_of_nodes + 1): + tgt_node = random.choice(nodes) + src_node = {"data": {"id": src_node_id, "label": src_node_id}} + + new_conn = {"data": {"source": src_node_id, "target": tgt_node["data"]["id"]}} + + nodes.append(src_node) + conns.append(new_conn) + + return nodes + conns + + +idom.run(RandomNetworkGraph) diff --git a/docs/source/index.rst b/docs/source/index.rst index 3ab4e2177..a83bd26a2 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -70,12 +70,12 @@ A package for building responsive user interfaces in pure Python. .. grid-item-card:: - .. interactive-widget:: snake_game + .. interactive-widget:: network_graph :no-activate-button: .. grid-item-card:: - .. interactive-widget:: cytoscape + .. interactive-widget:: snake_game :no-activate-button: .. grid-item-card:: From a5eb2461d8d47429e2b1c86b14e322f7395c5d9f Mon Sep 17 00:00:00 2001 From: rmorshea Date: Thu, 30 Sep 2021 12:31:42 -0700 Subject: [PATCH 6/6] remove custom templating solution --- src/idom/web/module.py | 29 +++----------- src/idom/web/templates/react.default.js | 51 +++++++++++++++++++++++-- 2 files changed, 52 insertions(+), 28 deletions(-) diff --git a/src/idom/web/module.py b/src/idom/web/module.py index dd51b957e..32e5dcc9d 100644 --- a/src/idom/web/module.py +++ b/src/idom/web/module.py @@ -9,7 +9,8 @@ from dataclasses import dataclass from functools import partial from pathlib import Path -from typing import Any, Dict, List, NewType, Optional, Set, Tuple, Union, overload +from string import Template +from typing import Any, List, NewType, Optional, Set, Tuple, Union, overload from urllib.parse import urlparse from typing_extensions import Protocol @@ -103,7 +104,6 @@ def module_from_template( **Templates** - ``react``: for modules exporting React components - - ``react-default``: for React modules that use ``export default`` Parameters: template: @@ -149,7 +149,9 @@ def module_from_template( if not target_file.exists(): target_file.parent.mkdir(parents=True, exist_ok=True) target_file.write_text( - _resolve_template(template_file, {"$PACKAGE": package, "$CDN": cdn}) + Template(template_file.read_text()).substitute( + {"PACKAGE": package, "CDN": cdn} + ) ) return WebModule( @@ -328,24 +330,3 @@ def _web_module_path(name: str, prefix: str = "") -> Path: directory /= prefix path = directory.joinpath(*name.split("/")) return path.with_suffix(path.suffix) - - -def _resolve_template(file: Path, substitutions: Dict[str, str]) -> str: - # NOTE: If this needs to be any more complex than it is, we should really - # reconsider this solution. Either use a real templating solution like Jinja - # or do something else entirely. - resolved_lines = [] - for line in file.read_text().splitlines(): - if line.startswith("$TEMPLATE:"): - relative_path = line.split(":", 1)[1].strip() - inner_template_file = file.parent.joinpath(*relative_path.split("/")) - resolved_lines.append(_resolve_template(inner_template_file, {})) - else: - resolved_lines.append(line) - - result = "\n".join(resolved_lines) - if substitutions: - for k, v in substitutions.items(): - result = result.replace(k, v) - - return result diff --git a/src/idom/web/templates/react.default.js b/src/idom/web/templates/react.default.js index 95f960cb1..bc450a4f6 100644 --- a/src/idom/web/templates/react.default.js +++ b/src/idom/web/templates/react.default.js @@ -1,5 +1,48 @@ -// add the default export -export { default } from "$CDN/$PACKAGE"; +export default from "$CDN/$PACKAGE"; +export * from "$CDN/$PACKAGE"; -// insert the normal react template -$TEMPLATE:react.js +import * as React from "$CDN/react"; +import * as ReactDOM from "$CDN/react-dom"; + +export function bind(node, config) { + return { + create: (component, props, children) => + React.createElement(component, wrapEventHandlers(props), ...children), + render: (element) => ReactDOM.render(element, node), + unmount: () => ReactDOM.unmountComponentAtNode(node), + }; +} + +function wrapEventHandlers(props) { + const newProps = Object.assign({}, props); + for (const [key, value] of Object.entries(props)) { + if (typeof value === "function") { + newProps[key] = makeJsonSafeEventHandler(value); + } + } + return newProps; +} + +function makeJsonSafeEventHandler(oldHandler) { + // Since we can't really know what the event handlers get passed we have to check if + // they are JSON serializable or not. We can allow normal synthetic events to pass + // through since the original handler already knows how to serialize those for us. + return function safeEventHandler() { + oldHandler( + ...Array.from(arguments).filter((value) => { + if (typeof value === "object" && value.nativeEvent) { + // this is probably a standard React synthetic event + return true; + } else { + try { + JSON.stringify(value); + } catch (err) { + console.error("Failed to serialize some event data"); + return false; + } + return true; + } + }) + ); + }; +}