#-----------------------------------------------------------------------------# Copyright (c) Anaconda, Inc., and Bokeh Contributors.# All rights reserved.## The full license is in the file LICENSE.txt, distributed with this software.#-----------------------------------------------------------------------------''' Provide functions and classes to help with various JS and CSS compilation.'''#-----------------------------------------------------------------------------# Boilerplate#-----------------------------------------------------------------------------from__future__importannotationsimportlogging# isort:skiplog=logging.getLogger(__name__)#-----------------------------------------------------------------------------# Imports#-----------------------------------------------------------------------------# Standard library importsimporthashlibimportjsonimportosimportreimportsysfromos.pathimport(abspath,dirname,exists,isabs,join,)frompathlibimportPathfromsubprocessimportPIPE,PopenfromtypingimportAny,Callable,Sequence# Bokeh importsfrom..core.has_propsimportHasPropsfrom..settingsimportsettingsfrom.stringsimportsnakify#-----------------------------------------------------------------------------# Globals and constants#-----------------------------------------------------------------------------__all__=('AttrDict','bundle_all_models','bundle_models','calc_cache_key','CompilationError','CustomModel','FromFile','get_cache_hook','Implementation','Inline','JavaScript','Less','nodejs_compile','nodejs_version','npmjs_version','set_cache_hook','TypeScript',)#-----------------------------------------------------------------------------# General API#-----------------------------------------------------------------------------
[docs]classAttrDict(dict[str,Any]):''' Provide a dict subclass that supports access by named attributes. '''def__getattr__(self,key:str)->Any:returnself[key]
[docs]classCompilationError(RuntimeError):''' A ``RuntimeError`` subclass for reporting JS compilation errors. '''def__init__(self,error:dict[str,str]|str)->None:super().__init__()ifisinstance(error,dict):self.line=error.get("line")self.column=error.get("column")self.message=error.get("message")self.text=error.get("text","")self.annotated=error.get("annotated")else:self.text=errordef__str__(self)->str:return"\n"+self.text.strip()
[docs]classImplementation:''' Base class for representing Bokeh custom model implementations. '''file:str|None=Nonecode:str@propertydeflang(self)->str:raiseNotImplementedError()
[docs]classInline(Implementation):''' Base class for representing Bokeh custom model implementations that may be given as inline code in some language. Args: code (str) : The source code for the implementation file (str, optional) A file path to a file containing the source text (default: None) '''def__init__(self,code:str,file:str|None=None)->None:self.code=codeself.file=file
[docs]classTypeScript(Inline):''' An implementation for a Bokeh custom model in TypeScript Example: .. code-block:: python class MyExt(Model): __implementation__ = TypeScript(""" <TypeScript code> """) '''@propertydeflang(self)->str:return"typescript"
[docs]classJavaScript(Inline):''' An implementation for a Bokeh custom model in JavaScript Example: .. code-block:: python class MyExt(Model): __implementation__ = JavaScript(""" <JavaScript code> """) '''@propertydeflang(self)->str:return"javascript"
[docs]classLess(Inline):''' An implementation of a Less CSS style sheet. '''@propertydeflang(self)->str:return"less"
[docs]classFromFile(Implementation):''' A custom model implementation read from a separate source file. Args: path (str) : The path to the file containing the extension source code '''def__init__(self,path:str)->None:withopen(path,encoding="utf-8")asf:self.code=f.read()self.file=path@propertydeflang(self)->str:ifself.fileisnotNone:ifself.file.endswith(".ts"):return"typescript"ifself.file.endswith(".js"):return"javascript"ifself.file.endswith((".css",".less")):return"less"raiseValueError(f"unknown file type {self.file}")
#: recognized extensions that can be compiledexts=(".ts",".js",".css",".less")
[docs]classCustomModel:''' Represent a custom (user-defined) Bokeh model. '''def__init__(self,cls:type[HasProps])->None:self.cls=cls@propertydefname(self)->str:returnself.cls.__name__@propertydeffull_name(self)->str:name=self.cls.__module__+"."+self.namereturnname.replace("__main__.","")@propertydeffile(self)->str|None:module=sys.modules[self.cls.__module__]ifhasattr(module,"__file__")and(file:=module.__file__)isnotNone:returnabspath(file)else:returnNone@propertydefpath(self)->str:path=getattr(self.cls,"__base_path__",None)ifpathisnotNone:returnpathelifself.fileisnotNone:returndirname(self.file)else:returnos.getcwd()@propertydefimplementation(self)->Implementation:impl=getattr(self.cls,"__implementation__")ifisinstance(impl,str):if"\n"notinimplandimpl.endswith(exts):impl=FromFile(implifisabs(impl)elsejoin(self.path,impl))else:impl=TypeScript(impl)ifisinstance(impl,Inline)andimpl.fileisNone:file=f"{self.file+':'ifself.fileelse''}{self.name}.ts"impl=impl.__class__(impl.code,file)returnimpl@propertydefdependencies(self)->dict[str,str]:returngetattr(self.cls,"__dependencies__",{})@propertydefmodule(self)->str:returnf"custom/{snakify(self.full_name)}"
[docs]defget_cache_hook()->Callable[[CustomModel,Implementation],AttrDict|None]:'''Returns the current cache hook used to look up the compiled code given the CustomModel and Implementation'''return_CACHING_IMPLEMENTATION
[docs]defset_cache_hook(hook:Callable[[CustomModel,Implementation],AttrDict|None])->None:'''Sets a compiled model cache hook used to look up the compiled code given the CustomModel and Implementation'''global_CACHING_IMPLEMENTATION_CACHING_IMPLEMENTATION=hook
[docs]defcalc_cache_key(custom_models:dict[str,CustomModel])->str:''' Generate a key to cache a custom extension implementation with. There is no metadata other than the Model classes, so this is the only base to generate a cache key. We build the model keys from the list of ``model.full_name``. This is not ideal but possibly a better solution can be found found later. '''model_names={model.full_nameformodelincustom_models.values()}encoded_names=",".join(sorted(model_names)).encode('utf-8')returnhashlib.sha256(encoded_names).hexdigest()
_bundle_cache:dict[str,str]={}
[docs]defbundle_models(models:Sequence[type[HasProps]]|None)->str|None:"""Create a bundle of selected `models`. """custom_models=_get_custom_models(models)ifcustom_modelsisNone:returnNonekey=calc_cache_key(custom_models)bundle=_bundle_cache.get(key,None)ifbundleisNone:try:_bundle_cache[key]=bundle=_bundle_models(custom_models)exceptCompilationErroraserror:print("Compilation failed:",file=sys.stderr)print(str(error),file=sys.stderr)sys.exit(1)returnbundle
[docs]defbundle_all_models()->str|None:"""Create a bundle of all models. """returnbundle_models(None)
#-----------------------------------------------------------------------------# Dev API#-----------------------------------------------------------------------------#-----------------------------------------------------------------------------# Private API#-----------------------------------------------------------------------------_plugin_umd= \
"""\(function(root, factory) { factory(root["Bokeh"]);})(this, function(Bokeh) { let define; return %(content)s;});"""# XXX: this is (almost) the same as bokehjs/src/js/plugin-prelude.js_plugin_prelude= \
"""\(function outer(modules, entry) { if (Bokeh != null) { return Bokeh.register_plugin(modules, entry); } else { throw new Error("Cannot find Bokeh. You have to load it prior to loading plugins."); }})"""_plugin_template= \
"""\%(prelude)s\({ "custom/main": function(require, module, exports) { const models = { %(exports)s }; require("base").register_models(models); module.exports = models; }, %(modules)s}, "custom/main");"""_style_template= \
"""\(function() { const head = document.getElementsByTagName('head')[0]; const style = document.createElement('style'); style.type = 'text/css'; const css = %(css)s; if (style.styleSheet) { style.styleSheet.cssText = css; } else { style.appendChild(document.createTextNode(css)); } head.appendChild(style);}());"""_export_template= \
""""%(name)s": require("%(module)s").%(name)s"""_module_template= \
""""%(module)s": function(require, module, exports) {\n%(source)s\n}"""def_detect_nodejs()->Path:nodejs_path=settings.nodejs_path()nodejs_paths=[nodejs_path]ifnodejs_pathisnotNoneelse["nodejs","node"]fornodejs_pathinnodejs_paths:try:proc=Popen([nodejs_path,"--version"],stdout=PIPE,stderr=PIPE)(stdout,_)=proc.communicate()exceptOSError:continueifproc.returncode!=0:continuematch=re.match(r"^v(\d+)\.(\d+)\.(\d+).*$",stdout.decode("utf-8"))ifmatchisnotNone:version=tuple(int(v)forvinmatch.groups())ifversion>=nodejs_min_version:returnPath(nodejs_path)# if we've reached here, no valid version was foundversion_repr=".".join(str(x)forxinnodejs_min_version)raiseRuntimeError(f'node.js v{version_repr} or higher is needed to allow compilation of custom models '+'("conda install nodejs" or follow https://nodejs.org/en/download/)')_nodejs:Path|None=None_npmjs:Path|None=Nonedef_nodejs_path()->Path:global_nodejsif_nodejsisNone:_nodejs=_detect_nodejs()return_nodejsdef_npmjs_path()->Path:global_npmjsif_npmjsisNone:executable="npm.cmd"ifsys.platform=="win32"else"npm"_npmjs=_nodejs_path().parent/executablereturn_npmjsdef_crlf_cr_2_lf(s:str)->str:returnre.sub(r"\\r\\n|\\r|\\n",r"\\n",s)def_run(app:Path,argv:list[str],input:dict[str,Any]|None=None)->str:proc=Popen([app,*argv],stdout=PIPE,stderr=PIPE,stdin=PIPE)(stdout,errout)=proc.communicate(input=NoneifinputisNoneelsejson.dumps(input).encode())ifproc.returncode!=0:raiseRuntimeError(errout.decode('utf-8'))else:return_crlf_cr_2_lf(stdout.decode('utf-8'))def_run_nodejs(argv:list[str],input:dict[str,Any]|None=None)->str:return_run(_nodejs_path(),argv,input)def_run_npmjs(argv:list[str],input:dict[str,Any]|None=None)->str:return_run(_npmjs_path(),argv,input)def_version(run_app:Callable[[list[str],dict[str,Any]|None],str])->str|None:try:version=run_app(["--version"],None)# explicit None to make mypy happyexceptRuntimeError:returnNoneelse:returnversion.strip()def_model_cache_no_op(model:CustomModel,implementation:Implementation)->AttrDict|None:"""Return cached compiled implementation"""returnNone_CACHING_IMPLEMENTATION=_model_cache_no_opdef_get_custom_models(models:Sequence[type[HasProps]]|None)->dict[str,CustomModel]|None:"""Returns CustomModels for models with a custom `__implementation__`"""custom_models:dict[str,CustomModel]=dict()forclsinmodelsorHasProps.model_class_reverse_map.values():impl=getattr(cls,"__implementation__",None)ifimplisnotNone:model=CustomModel(cls)custom_models[model.full_name]=modelreturncustom_modelsifcustom_modelselseNonedef_compile_models(custom_models:dict[str,CustomModel])->dict[str,AttrDict]:"""Returns the compiled implementation of supplied `models`. """ordered_models=sorted(custom_models.values(),key=lambdamodel:model.full_name)custom_impls={}dependencies:list[tuple[str,str]]=[]formodelinordered_models:dependencies.extend(list(model.dependencies.items()))ifdependencies:dependencies=sorted(dependencies,key=lambdaname_version:name_version[0])_run_npmjs(["install","--no-progress"]+[name+"@"+versionfor(name,version)independencies])formodelinordered_models:impl=model.implementationcompiled=_CACHING_IMPLEMENTATION(model,impl)ifcompiledisNone:compiled=nodejs_compile(impl.code,lang=impl.lang,file=impl.file)if"error"incompiled:raiseCompilationError(compiled.error)custom_impls[model.full_name]=compiledreturncustom_implsdef_bundle_models(custom_models:dict[str,CustomModel])->str:""" Create a JavaScript bundle with selected `models`. """exports=[]modules=[]lib_dir=Path(bokehjs_dir)/"js"/"lib"known_modules:set[str]=set()forpathinlib_dir.rglob("*.d.ts"):s=str(path.relative_to(lib_dir))ifs.endswith(".d.ts"):s=s[:-5]# TODO: removesuffix() (Py 3.9+)s=s.replace(os.path.sep,"/")known_modules.add(s)custom_impls=_compile_models(custom_models)extra_modules={}defresolve_modules(to_resolve:set[str],root:str)->dict[str,str]:resolved={}formoduleinto_resolve:ifmodule.startswith(("./","../")):defmkpath(module:str,ext:str="")->str:returnabspath(join(root,*module.split("/"))+ext)ifmodule.endswith(exts):path=mkpath(module)ifnotexists(path):raiseRuntimeError("no such module: %s"%module)else:forextinexts:path=mkpath(module,ext)ifexists(path):breakelse:raiseRuntimeError("no such module: %s"%module)impl=FromFile(path)compiled=nodejs_compile(impl.code,lang=impl.lang,file=impl.file)ifimpl.lang=="less":code=_style_template%dict(css=json.dumps(compiled.code))deps=[]else:code=compiled.codedeps=compiled.depssig=hashlib.sha256(code.encode('utf-8')).hexdigest()resolved[module]=sigdeps_map=resolve_deps(deps,dirname(path))ifsignotinextra_modules:extra_modules[sig]=Truemodules.append((sig,code,deps_map))else:index=module+(""ifmodule.endswith("/")else"/")+"index"ifindexnotinknown_modules:raiseRuntimeError("no such module: %s"%module)returnresolveddefresolve_deps(deps:list[str],root:str)->dict[str,str]:custom_modules={model.moduleformodelincustom_models.values()}missing=set(deps)-known_modules-custom_modulesreturnresolve_modules(missing,root)formodelincustom_models.values():compiled=custom_impls[model.full_name]deps_map=resolve_deps(compiled.deps,model.path)exports.append((model.name,model.module))modules.append((model.module,compiled.code,deps_map))# sort everything by module nameexports=sorted(exports,key=lambdaspec:spec[1])modules=sorted(modules,key=lambdaspec:spec[0])bare_modules=[]fori,(module,code,deps)inenumerate(modules):forname,refindeps.items():code=code.replace("""require("%s")"""%name,"""require("%s")"""%ref)code=code.replace("""require('%s')"""%name,"""require('%s')"""%ref)bare_modules.append((module,code))sep=",\n"rendered_exports=sep.join(_export_template%dict(name=name,module=module)for(name,module)inexports)rendered_modules=sep.join(_module_template%dict(module=module,source=code)for(module,code)inbare_modules)content=_plugin_template%dict(prelude=_plugin_prelude,exports=rendered_exports,modules=rendered_modules)return_plugin_umd%dict(content=content)#-----------------------------------------------------------------------------# Code#-----------------------------------------------------------------------------