Skip to content

make_modules doesn't scan local modules for dependencies #2567

Open
@CookiePLMonster

Description

@CookiePLMonster

At https://cookieplmonster.github.io/bonuscodes/toca-race-driver-3/ I have an inline Python script that imports a few more Python modules I have in my website's PYTHONPATH. Some of those modules in turn import more Brython and Python modules:

  • htmlgen.py imports Template from browser.template.
  • rd3.py imports struct and collections.namedtuple.

This setup works perfectly with brython_stdlib.js, with the exception of the rd2 module returning a single 404 (visible in the Console), but that is expected as per the way Brython works (or maybemake_modules is intended to solve this too?), but with python_modules.js generated stops working.

Running make_modules in the root directory of my blog produces this brython_modules.js file - as you can see, it's missing template, struct and namedtuple:

brython_modules.js
__BRYTHON__.VFS_timestamp = 1744651669804
if(typeof document !== 'undefined'){
    __BRYTHON__.brython_modules = $B.last(document.getElementsByTagName('script')).src
}
__BRYTHON__.use_VFS = true
var scripts = {"$timestamp": 1744651669803, "_ajax": [".js", "// ajax\n__BRYTHON__.imported._ajax = (function($B){\n\n\nvar $N = $B.builtins.None,\n    _b_ = $B.builtins\n\nvar add_to_res = function(res, key, val) {\n    if($B.$isinstance(val, _b_.list)){\n        for (j = 0; j < val.length; j++) {\n            add_to_res(res, key, val[j])\n        }\n    }else if (val instanceof File || val instanceof Blob){\n        res.append(key, val)\n    }else{\n        res.append(key, _b_.str.$factory(val))\n    }\n}\n\nfunction set_timeout(self, timeout){\n    if(timeout.seconds !== undefined){\n        self.js.$requestTimer = setTimeout(\n            function() {\n                self.js.abort()\n                if(timeout.func){\n                    timeout.func()\n                }\n            },\n            timeout.seconds * 1000)\n    }\n}\n\nfunction _read(req){\n    var xhr = req.js\n    if(xhr.responseType == \"json\"){\n        return $B.structuredclone2pyobj(xhr.response)\n    }\n    if(req.charset_user_defined){\n        // on blocking mode, xhr.response is a string\n        var bytes = []\n        for(var i = 0, len = xhr.response.length; i < len; i++){\n            var cp = xhr.response.codePointAt(i)\n            if(cp > 0xf700){\n                bytes.push(cp - 0xf700)\n            }else{\n                bytes.push(cp)\n            }\n        }\n    }else if(typeof xhr.response == \"string\"){\n        if(req.mode == 'binary'){\n            return _b_.str.encode(xhr.response,\n                $B.$getattr(req, 'encoding', 'utf-8'))\n        }\n        return xhr.response\n    }else{\n        // else it's an ArrayBuffer\n        var buf = new Uint8Array(xhr.response),\n            bytes = Array.from(buf.values())\n    }\n    var b = _b_.bytes.$factory(bytes),\n        mode = $B.$getattr(req, 'mode', null)\n    if(mode == \"binary\"){\n        return b\n    }else if(mode == \"document\"){\n        return $B.jsobj2pyobj(xhr.response)\n    }else{\n        var encoding = $B.$getattr(req, 'encoding', \"utf-8\")\n        return _b_.bytes.decode(b, encoding)\n    }\n}\n\nfunction stringify(d){\n    var items = []\n    for(var entry of _b_.dict.$iter_items(d)){\n        items.push(encodeURIComponent(entry.key) + \"=\" +\n                   encodeURIComponent(entry.value))\n    }\n    return items.join(\"&\")\n}\n\nfunction handle_kwargs(self, kw, method){\n    var data,\n        encoding,\n        headers = {},\n        cache,\n        mode = \"text\",\n        timeout = {},\n        rawdata\n\n    for(var item of _b_.dict.$iter_items(kw)){\n        var key = item.key\n        if(key == \"data\"){\n            var rawdata = item.value\n            if(typeof rawdata == \"string\" || rawdata instanceof FormData){\n                data = rawdata\n            }else if(rawdata.__class__ === _b_.dict){\n                data = stringify(rawdata)\n            }else{\n                throw _b_.TypeError.$factory(\"wrong type for data: \" +\n                    $B.class_name(rawdata))\n            }\n        }else if(key == \"encoding\"){\n            encoding = item.value\n        }else if(key == \"headers\"){\n            var value = item.value\n            if(! $B.$isinstance(value, _b_.dict)){\n                throw _b_.ValueError.$factory(\n                    \"headers must be a dict, not \" + $B.class_name(value))\n            }\n            for(var subitem of _b_.dict.$iter_items(value)){\n                headers[subitem.key.toLowerCase()] = subitem.value\n            }\n        }else if(key.startsWith(\"on\")){\n            var event = key.substr(2)\n            if(event == \"timeout\"){\n                timeout.func = item.value\n            }else{\n                var f = item.value\n                ajax.bind(self, event, f)\n            }\n        }else if(key == \"mode\"){\n            var mode = item.value\n        }else if(key == \"timeout\"){\n            timeout.seconds = item.value\n        }else if(key == \"cache\"){\n            cache = item.value\n        }\n    }\n    if(encoding && mode != \"text\"){\n        throw _b_.ValueError.$factory(\"encoding not supported for mode \" +\n            mode)\n    }\n    if((method == \"post\" || method == \"put\") && ! headers){\n        // For POST requests, set default header\n        self.js.setRequestHeader(\"Content-type\",\n                                 \"application/x-www-form-urlencoded\")\n    }\n\n    return {cache, data, rawdata, encoding, headers, mode, timeout}\n}\n\nvar ajax = $B.make_class('ajax')\n\najax.__repr__ = function(self){\n    return '<object Ajax>'\n}\n\najax.__getattribute__ = function(self, attr){\n    if(ajax[attr] !== undefined){\n        return function(){\n            return ajax[attr].call(null, self, ...arguments)\n        }\n    }else if(attr == \"text\"){\n        return _read(self)\n    }else if(attr == \"json\"){\n        if(self.js.responseType == \"json\"){\n            return _read(self)\n        }else{\n            var resp = _read(self)\n            try{\n                return $B.structuredclone2pyobj(JSON.parse(resp))\n            }catch(err){\n                console.log('attr json, invalid resp', resp)\n                throw err\n            }\n        }\n    }else if(self.js[attr] !== undefined){\n        if(typeof self.js[attr] == \"function\"){\n            return function(){\n                if(attr == \"setRequestHeader\"){\n                    ajax.set_header.call(null, self, ...arguments)\n                }else{\n                    if(attr == 'overrideMimeType'){\n                        console.log('override mime type')\n                        self.hasMimeType = true\n                    }\n                    return self.js[attr](...arguments)\n                }\n            }\n        }else{\n            return self.js[attr]\n        }\n    }else if(attr == \"xml\"){\n        return $B.jsobj2pyobj(self.js.responseXML)\n    }\n    return _b_.object.__getattribute__(self, attr)\n}\n\najax.bind = function(self, evt, func){\n    // req.bind(evt,func) is the same as req.onevt = func\n    self.js['on' + evt] = function(){\n        try{\n            return func.apply(null, arguments)\n        }catch(err){\n            $B.handle_error(err)\n        }\n    }\n    return _b_.None\n}\n\najax.open = function(){\n    var $ = $B.args('open', 4,\n            {self: null, method: null, url: null, async: null},\n            ['self', 'method', 'url', 'async'], arguments,\n            {async: true}, null, null),\n        self = $.self,\n        method = $.method,\n        url = $.url,\n        async = $.async\n    if(typeof method !== \"string\"){\n        throw _b_.TypeError.$factory(\n            'open() argument method should be string, got ' +\n            $B.class_name(method))\n    }\n    if(typeof url !== \"string\"){\n        throw _b_.TypeError.$factory(\n            'open() argument url should be string, got ' +\n            $B.class_name(url))\n    }\n    self.$method = method\n    self.blocking = ! self.async\n    self.js.open(method, url, async)\n}\n\najax.read = function(self){\n    return _read(self)\n}\n\najax.send = function(self, params){\n    // params can be Python dictionary or string\n    var content_type\n    for(var key in self.headers){\n        var value = self.headers[key]\n        self.js.setRequestHeader(key, value)\n        if(key == 'content-type'){\n            content_type = value\n        }\n    }\n    if(($B.$getattr(self, 'encoding', false) ||\n            $B.$getattr(self, 'blocking', false)) && ! self.hasMimeType){\n        // On blocking mode, or if an encoding has been specified,\n        // override Mime type so that bytes are not processed\n        // (unless the Mime type has been explicitely set)\n        self.js.overrideMimeType('text/plain;charset=x-user-defined')\n        self.charset_user_defined = true\n    }\n    var res = ''\n    if(! params){\n        self.js.send()\n        return _b_.None\n    }\n    if($B.$isinstance(params, _b_.str)){\n        res = params\n    }else if($B.$isinstance(params, _b_.dict)){\n        if(content_type == 'multipart/form-data'){\n            // The FormData object serializes the data in the 'multipart/form-data'\n            // content-type so we may as well override that header if it was set\n            // by the user.\n            res = new FormData()\n            var items = _b_.list.$factory(_b_.dict.items(params))\n            for(var i = 0, len = items.length; i < len; i++){\n                add_to_res(res, _b_.str.$factory(items[i][0]), items[i][1])\n            }\n        }else{\n            if(self.$method && self.$method.toUpperCase() == \"POST\" &&\n                    ! content_type){\n                // Set default Content-Type for POST requests\n                self.js.setRequestHeader(\"Content-Type\",\n                    \"application/x-www-form-urlencoded\")\n            }\n            var items = _b_.list.$factory(_b_.dict.items(params))\n            for(var i = 0, len = items.length; i < len; i++){\n                var key = encodeURIComponent(_b_.str.$factory(items[i][0]));\n                if($B.$isinstance(items[i][1], _b_.list)){\n                    for (j = 0; j < items[i][1].length; j++) {\n                        res += key +'=' +\n                            encodeURIComponent(_b_.str.$factory(items[i][1][j])) + '&'\n                    }\n                }else{\n                    res += key + '=' +\n                        encodeURIComponent(_b_.str.$factory(items[i][1])) + '&'\n                }\n            }\n            res = res.substr(0, res.length - 1)\n        }\n    }else if(params instanceof FormData){\n        res = params\n    }else{\n        throw _b_.TypeError.$factory(\n            \"send() argument must be string or dictionary, not '\" +\n            _b_.str.$factory(params.__class__) + \"'\")\n    }\n    self.js.send(res)\n    return _b_.None\n}\n\najax.responseType = _b_.property.$factory(\n    function(_self){\n        return _self.responseType\n    },\n    function(_self, value){\n        _self.js.responseType = value\n    }\n)\n\najax.withCredentials = _b_.property.$factory(\n    function(_self){\n        return _self.withCredentials\n    },\n    function(_self, value){\n        _self.js.withCredentials = value\n    }\n)\n\najax.set_header = function(self, key, value){\n    self.headers[key.toLowerCase()] = value\n}\n\najax.set_timeout = function(self, seconds, func){\n    self.js.$requestTimer = setTimeout(\n        function() {\n            self.js.abort()\n            func()\n        },\n        seconds * 1000)\n}\n\najax.$factory = function(){\n\n    var xmlhttp = new XMLHttpRequest()\n\n    xmlhttp.onreadystatechange = function(){\n        // here, \"this\" refers to xmlhttp\n        var state = this.readyState\n        if(this.responseType == \"\" || this.responseType == \"text\"){\n            res.js.text = this.responseText\n        }\n        var timer = this.$requestTimer\n        if(state == 0 && this.onuninitialized){\n            this.onuninitialized(res)\n        }else if(state == 1 && this.onloading){\n            this.onloading(res)\n        }else if(state == 2 && this.onloaded){\n            this.onloaded(res)\n        }else if(state == 3 && this.oninteractive){\n            this.oninteractive(res)\n        }else if(state == 4 && this.oncomplete){\n            if(timer !== null){\n                globalThis.clearTimeout(timer)\n            }\n            this.oncomplete(res)\n        }\n    }\n    var res = {\n        __class__: ajax,\n        __dict__: $B.empty_dict(),\n        js: xmlhttp,\n        headers: {}\n    }\n    return res\n}\n\n\nfunction _request_without_body(method){\n    var $ = $B.args(method, 3, {method: null, url: null, blocking: null},\n        [\"method\", \"url\", \"blocking\"], arguments, {blocking: false},\n        null, \"kw\"),\n    method = $.method,\n    url = $.url,\n    async = !$.blocking,\n    kw = $.kw\n\n    var self = ajax.$factory()\n    self.blocking = $.blocking\n    var items = handle_kwargs(self, kw, method),\n        mode = items.mode,\n        encoding = items.encoding,\n        qs = items.data\n    $B.$setattr(self, 'mode', mode)\n    $B.$setattr(self, 'encoding', encoding)\n    if(qs){\n        url += \"?\" + qs\n    }\n    if(! (items.cache === true)){\n        url += (qs ? \"&\" : \"?\") + (new Date()).getTime()\n    }\n    self.js.open(method.toUpperCase(), url, async)\n\n    if(async){\n        if(mode == \"json\" || mode == \"document\"){\n            self.js.responseType = mode\n        }else{\n            self.js.responseType = \"arraybuffer\"\n            if(mode != \"text\" && mode != \"binary\"){\n                throw _b_.ValueError.$factory(\"invalid mode: \" + mode)\n            }\n        }\n    }else{\n        self.js.overrideMimeType('text/plain;charset=x-user-defined')\n        self.charset_user_defined = true\n    }\n    for(var key in items.headers){\n        self.js.setRequestHeader(key, items.headers[key])\n    }\n    var timeout = items.timeout\n    if(timeout.seconds){\n        ajax.set_timeout(self, timeout.seconds, timeout.func)\n    }\n    // Add function read() to return str or bytes according to mode\n    self.js.send()\n}\n\nfunction _request_with_body(method){\n    var $ = $B.args(method, 3, {method: null, url: null, blocking: null},\n        [\"method\", \"url\", \"blocking\"], arguments, {blocking: false},\n        null, \"kw\"),\n        method = $.method,\n        url = $.url,\n        async = !$.blocking,\n        kw = $.kw,\n        content_type\n    var self = ajax.$factory()\n    self.js.open(method.toUpperCase(), url, async)\n    var items = handle_kwargs(self, kw, method), // common with browser.aio\n        data = items.data\n\n    if($B.$isinstance(data, _b_.dict)){\n        data = stringify(data)\n    }\n    for(var key in items.headers){\n        var value = items.headers[key]\n        self.js.setRequestHeader(key, value)\n        if(key == 'content-type'){\n            content_type = value\n        }\n    }\n    if(method.toUpperCase() == 'POST' && !content_type){\n        // set default Content-Type for POST requests\n        self.js.setRequestHeader('Content-Type',\n            'application/x-www-form-urlencoded')\n    }\n\n    // Add function read() to return str or bytes according to mode\n    self.js.read = function(){\n        return _read(self)\n    }\n    self.js.send(data)\n}\n\nfunction form_data(form){\n    var missing = {},\n        $ = $B.args('form_data', 1, {form: null}, ['form'], arguments,\n            {form: missing}, null, null)\n    if($.form === missing){\n        return new FormData()\n    }else{\n        return new FormData($.form)\n    }\n}\n\nfunction connect(){\n    _request_without_body.call(null, \"connect\", ...arguments)\n}\n\nfunction _delete(){\n    _request_without_body.call(null, \"delete\", ...arguments)\n}\n\nfunction get(){\n    _request_without_body.call(null, \"get\", ...arguments)\n}\n\nfunction head(){\n    _request_without_body.call(null, \"head\", ...arguments)\n}\n\nfunction options(){\n    _request_without_body.call(null, \"options\", ...arguments)\n}\n\nfunction patch(){\n    _request_with_body.call(null, \"put\", ...arguments)\n}\n\nfunction post(){\n    _request_with_body.call(null, \"post\", ...arguments)\n}\n\nfunction put(){\n    _request_with_body.call(null, \"put\", ...arguments)\n}\n\nfunction trace(){\n    _request_without_body.call(null, \"trace\", ...arguments)\n}\n\nfunction file_upload(){\n    // ajax.file_upload(url, file, method=\"POST\", **callbacks)\n    var $ = $B.args(\"file_upload\", 2, {url: null, \"file\": file},\n            [\"url\", \"file\"], arguments, {}, null, \"kw\"),\n        url = $.url,\n        file = $.file,\n        kw = $.kw\n\n    var self = ajax.$factory()\n\n    var items = handle_kwargs(self, kw, method),\n        rawdata = items.rawdata,\n        headers = items.headers\n\n    for(var key in headers){\n        var value = headers[key]\n        self.js.setRequestHeader(key, value)\n        if(key == 'content-type'){\n            content_type = value\n        }\n    }\n\n    var timeout = items.timeout\n    if(timeout.seconds){\n        ajax.set_timeout(self, timeout.seconds, timeout.func)\n    }\n\n    var method = _b_.dict.$get_string(kw, 'method', 'POST'),\n        field_name = _b_.dict.$get_string(kw, 'field_name', 'filetosave')\n\n    var formdata = new FormData()\n    formdata.append(field_name, file, file.name)\n\n    if(rawdata){\n        if(rawdata instanceof FormData){\n            // append additional data\n            for(var d of rawdata){\n                formdata.append(d[0], d[1])\n            }\n        }else if($B.$isinstance(rawdata, _b_.dict)){\n            for(var item of _b_.dict.$iter_items(rawdata)){\n                formdata.append(item.key, item.value)\n            }\n        }else{\n            throw _b_.ValueError.$factory(\n                'data value must be a dict of form_data')\n        }\n    }\n\n    self.js.open(method, url, _b_.True)\n    self.js.send(formdata)\n\n}\n\n$B.set_func_names(ajax)\n\nreturn {\n    ajax: ajax,\n    Ajax: ajax,\n    delete: _delete,\n    file_upload: file_upload,\n    connect,\n    form_data,\n    get,\n    head,\n    options,\n    patch,\n    post,\n    put,\n    trace\n}\n\n})(__BRYTHON__)\n"], "browser": [".py", "", [], 1], "browser.ajax": [".py", "from _ajax import *\n", ["_ajax"]]}
__BRYTHON__.update_VFS(scripts)

If I explicitly import any of those modules from one of my HTML files, make_modules gathers the dependency correctly.

My modules directory, added to PYTHONPATH with <brython-options pythonpath="/assets/brython">, is here - it exists on the deployed site, but isn't considered for the module scan:
https://github.com/CookiePLMonster/CookiePLMonster.github.io/tree/master/assets/brython

EDIT:
Perhaps alongside .py files, .bry files should also be scanned for dependencies, considering this specific "fake" extension is mentioned in the documentation?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions