Skip to content

PAC support

proxylib.pac

PAC (Proxy Auto-Config) support: the standard Netscape utility functions a PAC script's FindProxyForURL relies on, plus the common Microsoft *Ex/IPv6-aware extensions, and loading PAC scripts (as plain Python via subclassing, or as real JS via the optional dukpy backend).

__all__ = ('PAC', 'load', 'clear_download_cache', 'clear_dns_cache') module-attribute

JSProxyAutoConfig(js, overrides=None)

Bases: PAC, JSContext

A PAC whose FindProxyForURL (and the utility functions above) run as real JS.

Source code in src/proxylib/pac/javascript.py
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
def __init__(self, js: str, overrides: "Optional[Dict[str, Callable]]" = None) -> None:
    context: dict = object.__getattribute__(self, "_JSCONTEXT")
    if overrides:
        context = dict(context)
        # Wrapped as staticmethod so the binding loop below treats them
        # exactly like the class's own PAC utility functions (unwrapped
        # via val.__func__, no `self` bound) -- overrides are plain
        # standalone callables (e.g. `lambda host: "10.0.0.1"`), not
        # instance methods, so val.__get__(self) below would wrongly
        # bind `self` as an implicit first argument otherwise.
        context.update({key: staticmethod(fn) for key, fn in overrides.items()})
        # Stored per-instance, not mutated on the shared class-level
        # _JSCONTEXT, so overrides from one instance don't leak into
        # another's -- __getattribute__ below picks this up too, so an
        # override key that isn't one of the base PAC methods is still
        # recognized as exported.
        object.__setattr__(self, "_JSCONTEXT", context)
    engine_cls = get_engine_class()
    if engine_cls is None:
        raise ImportError(
            "No PAC JS engine is installed -- install one of the optional "
            "extras: proxylib[jspac] (dukpy) or proxylib[quickjs]."
        )
    engine = engine_cls()
    for key, val in context.items():
        if isinstance(val, staticmethod):
            val = val.__func__
        elif isinstance(val, classmethod):
            val = val.__get__(self.__class__)
        elif isinstance(val, property):
            raise NotImplementedError("JSContext does not support property exports yet")
        else:
            val = val.__get__(self)

        engine.export_function(key, val)
    engine.eval(js)

    # Pre-bind one callable per exported name now, instead of allocating
    # a fresh closure on every attribute access. Each one still calls
    # engine.call(key, ...), which resolves `key` fresh in the JS
    # engine's *global scope* at call time (not at bind time) -- that's
    # what actually implements "JS override wins": if `js` redefined
    # `key`, the engine resolves to that redefinition regardless of
    # when this Python-side wrapper was created. A naive
    # `object.__setattr__(self, key, jsFunction)` here would never be
    # reached, though -- __getattribute__ below checks membership in
    # `context` *before* ever falling through to instance-attribute
    # lookup, so the bound callables live in their own dict instead.
    bound: "Dict[str, object]" = {}
    for key in context:

        def _make_js_function(key=key):
            def jsFunction(*args):
                return engine.call(key, *args)

            return jsFunction

        bound[key] = _make_js_function()

    object.__setattr__(self, "_jsengine", engine)
    object.__setattr__(self, "_bound_js_functions", bound)

PAC

Bases: object

Base implementation of the PAC utility-function namespace.

FindProxyForURL here always returns "DIRECT"; subclass and override it (or use :class:JSProxyAutoConfig to run a real PAC script) to actually select proxies.

FindProxyForURL(url, host) staticmethod

Source code in src/proxylib/pac/__init__.py
312
313
314
@staticmethod
def FindProxyForURL(url: str, host: str, /) -> str:
    return "DIRECT"

__contains__(key)

Source code in src/proxylib/pac/__init__.py
332
333
334
335
336
337
def __contains__(self, key: object) -> bool:
    try:
        self[key]
        return True
    except KeyError:
        return False

__getitem__(url)

Source code in src/proxylib/pac/__init__.py
316
317
318
319
320
321
322
323
324
def __getitem__(self, url: str) -> _Iter[_Optional[Proxy]]:
    # Pass the full URL (path and query included) -- that's what the PAC
    # spec's FindProxyForURL receives, and the whole reason the
    # requests/urllib integrations resolve per-request instead of
    # per-scheme. (Browsers strip https paths for privacy; a PAC file
    # you configure yourself is trusted with your own URLs.)
    parsed = urlparse(url)
    pac_proxies = self.FindProxyForURL(url, parsed.hostname or "")
    return Proxy.find_all(pac_proxies, UriSplit.PAC)

convert_addr(ipaddr) staticmethod

Source code in src/proxylib/pac/__init__.py
121
122
123
@staticmethod
def convert_addr(ipaddr: str, /) -> int:
    return int(_ip.ip_address(ipaddr))

dateRange(*args) staticmethod

Best-effort implementation of the PAC dateRange overload set.

Supports the documented call shapes: a single day/month/year, a day/month/year range, (day1, month1, day2, month2), (month1, year1, month2, year2) and (day1, month1, year1, day2, month2, year2), each optionally followed by "GMT".

Source code in src/proxylib/pac/__init__.py
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
@staticmethod
def dateRange(*args) -> bool:
    """Best-effort implementation of the PAC ``dateRange`` overload set.

    Supports the documented call shapes: a single day/month/year, a
    day/month/year range, ``(day1, month1, day2, month2)``,
    ``(month1, year1, month2, year2)`` and
    ``(day1, month1, year1, day2, month2, year2)``, each optionally
    followed by ``"GMT"``.
    """
    args = list(args)
    gmt = bool(args) and isinstance(args[-1], str) and args[-1].upper() == "GMT"
    if gmt:
        args = args[:-1]
    now = _datetime.datetime.now(_datetime.timezone.utc) if gmt else _datetime.datetime.now()

    def classify(value):
        if isinstance(value, str):
            return "month", _MONTHS.index(value.upper()) + 1
        value = int(value)
        return ("year", value) if value > 31 else ("day", value)

    parts = [classify(a) for a in args]
    current = {"day": now.day, "month": now.month, "year": now.year}
    today = (now.year, now.month, now.day)

    if len(parts) == 1:
        kind, val = parts[0]
        return current[kind] == val

    if len(parts) == 2 and parts[0][0] == parts[1][0]:
        kind = parts[0][0]
        lo, hi = parts[0][1], parts[1][1]
        value = current[kind]
        return lo <= value <= hi if lo <= hi else (value >= lo or value <= hi)

    if len(parts) == 4:
        kinds = [p[0] for p in parts]
        if kinds == ["day", "month", "day", "month"]:
            day1, month1, day2, month2 = (p[1] for p in parts)
            start, end = (now.year, month1, day1), (now.year, month2, day2)
        elif kinds == ["month", "year", "month", "year"]:
            month1, year1, month2, year2 = (p[1] for p in parts)
            start, end = (year1, month1, 1), (year2, month2, 31)
        else:
            return False
        return start <= today <= end if start <= end else (today >= start or today <= end)

    if len(parts) == 6:
        day1, month1, year1, day2, month2, year2 = (p[1] for p in parts)
        start, end = (year1, month1, day1), (year2, month2, day2)
        return start <= today <= end if start <= end else (today >= start or today <= end)

    return False

dnsDomainIs(host, domain) staticmethod

Source code in src/proxylib/pac/__init__.py
241
242
243
@staticmethod
def dnsDomainIs(host: str, domain: str) -> bool:
    return host.endswith(domain)

dnsDomainLevels(host) staticmethod

Number of dots in host (the PAC spec's definition), e.g. 2 for sub.example.com.

Source code in src/proxylib/pac/__init__.py
116
117
118
119
@staticmethod
def dnsDomainLevels(host: str, /) -> int:
    """Number of dots in ``host`` (the PAC spec's definition), e.g. 2 for ``sub.example.com``."""
    return host.count(".")

dnsResolve(host, /, cache_ttl=30.0) staticmethod

Resolve host to a single IPv4/IPv6 address string, or None.

Cached per host for cache_ttl seconds (default 30); pass cache_ttl=0/None to force a fresh resolution.

Source code in src/proxylib/pac/__init__.py
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
@staticmethod
def dnsResolve(host: str, /, cache_ttl: float = 30.0) -> "str|None":
    """Resolve host to a single IPv4/IPv6 address string, or None.

    Cached per host for ``cache_ttl`` seconds (default 30); pass
    ``cache_ttl=0``/``None`` to force a fresh resolution.
    """
    if cache_ttl:
        cached = _dns_cache.get(host)
        if cached is not None and (_time.monotonic() - cached[0]) < cache_ttl:
            return cached[1]
    ip = get_ip(host)
    result = ip.exploded if ip else None
    if cache_ttl:
        _dns_cache[host] = (_time.monotonic(), result)
    return result

dnsResolveEx(host) staticmethod

Resolve host to all of its addresses (IPv4 and IPv6), '; '-separated.

Source code in src/proxylib/pac/__init__.py
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
@staticmethod
def dnsResolveEx(host: str, /) -> str:
    """Resolve host to all of its addresses (IPv4 and IPv6), '; '-separated."""
    try:
        infos = _socket.getaddrinfo(host, None)
    except OSError:
        return ""
    seen: "list[str]" = []
    for info in infos:
        addr = info[4][0]
        if addr not in seen:
            seen.append(addr)
    return "; ".join(seen)

get(uri, default=None)

Source code in src/proxylib/pac/__init__.py
326
327
328
329
330
def get(self, uri: str, default=None):
    try:
        return self[uri]
    except KeyError:
        return default

getClientVersion() staticmethod

Microsoft extension: PAC engine version string.

Source code in src/proxylib/pac/__init__.py
307
308
309
310
@staticmethod
def getClientVersion() -> str:
    """Microsoft extension: PAC engine version string."""
    return "1.0"

isInNet(host, pattern, mask) staticmethod

IPv4 (spec) net-membership check, tolerant of IPv6 host/pattern too.

Source code in src/proxylib/pac/__init__.py
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
@staticmethod
def isInNet(host: str, pattern: str, mask: str) -> bool:
    """IPv4 (spec) net-membership check, tolerant of IPv6 host/pattern too."""
    try:
        ip = _ip.ip_address(host)
    except ValueError:
        resolved = PAC.dnsResolve(host)
        if resolved is None:
            return False
        try:
            ip = _ip.ip_address(resolved)
        except ValueError:
            return False
    try:
        net = _ip.ip_network(f"{pattern}/{mask}", strict=False)
    except ValueError:
        return False
    return ip in net

isInNetEx(ip_address, ip_prefix) staticmethod

Microsoft extension: CIDR-notation net check, IPv4 or IPv6.

Source code in src/proxylib/pac/__init__.py
287
288
289
290
291
292
293
294
295
@staticmethod
def isInNetEx(ip_address: str, ip_prefix: str) -> bool:
    """Microsoft extension: CIDR-notation net check, IPv4 or IPv6."""
    try:
        ip = _ip.ip_address(ip_address)
        net = _ip.ip_network(ip_prefix, strict=False)
    except ValueError:
        return False
    return ip in net

isPlainHostName(host) staticmethod

Source code in src/proxylib/pac/__init__.py
237
238
239
@staticmethod
def isPlainHostName(host: str) -> bool:
    return "." not in host

isResolvable(host) staticmethod

Source code in src/proxylib/pac/__init__.py
256
257
258
259
260
261
262
@staticmethod
def isResolvable(host: str) -> bool:
    try:
        _socket.gethostbyname(host)
        return True
    except OSError:
        return False

isResolvableEx(host) staticmethod

Source code in src/proxylib/pac/__init__.py
264
265
266
@staticmethod
def isResolvableEx(host: str) -> bool:
    return bool(PAC.dnsResolveEx(host))

localHostOrDomainIs(host, hostdom) staticmethod

True if host is either the bare hostname or the full host+domain of hostdom.

Compares the host part of hostdom exactly (not a prefix check) so e.g. "ww" does not wrongly match "www.example.com".

Source code in src/proxylib/pac/__init__.py
245
246
247
248
249
250
251
252
253
254
@staticmethod
def localHostOrDomainIs(host: str, hostdom: str) -> bool:
    """True if ``host`` is either the bare hostname or the full host+domain of ``hostdom``.

    Compares the *host part* of ``hostdom`` exactly (not a prefix check)
    so e.g. ``"ww"`` does not wrongly match ``"www.example.com"``.
    """
    if "." not in host:
        return hostdom.partition(".")[0] == host
    return hostdom == host

myIpAddress() staticmethod

Source code in src/proxylib/pac/__init__.py
104
105
106
107
108
109
@staticmethod
def myIpAddress() -> str:
    try:
        return _socket.gethostbyname(_socket.gethostname())
    except OSError:
        return "127.0.0.1"

myIpAddressEx() staticmethod

All local addresses (IPv4 and IPv6), '; '-separated.

Source code in src/proxylib/pac/__init__.py
111
112
113
114
@staticmethod
def myIpAddressEx() -> str:
    """All local addresses (IPv4 and IPv6), '; '-separated."""
    return PAC.dnsResolveEx(_socket.gethostname()) or PAC.myIpAddress()

shExpMatch(test, shexp) staticmethod

Source code in src/proxylib/pac/__init__.py
125
126
127
@staticmethod
def shExpMatch(test: str, shexp: str, /) -> bool:
    return _shexpmatch(test, shexp)

sortIpAddressList(ip_address_list) staticmethod

Microsoft extension: sort a '; '-separated address list numerically.

Source code in src/proxylib/pac/__init__.py
297
298
299
300
301
302
303
304
305
@staticmethod
def sortIpAddressList(ip_address_list: str) -> str:
    """Microsoft extension: sort a '; '-separated address list numerically."""
    addrs = [a.strip() for a in ip_address_list.split(";") if a.strip()]
    try:
        addrs.sort(key=_ip.ip_address)
    except ValueError:
        addrs.sort()
    return "; ".join(addrs)

timeRange(*args) staticmethod

PAC timeRange: hour, hour-hour, hour:min-hour:min, or hour:min:sec range, +GMT.

Source code in src/proxylib/pac/__init__.py
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
@staticmethod
def timeRange(*args) -> bool:
    """PAC ``timeRange``: hour, hour-hour, hour:min-hour:min, or hour:min:sec range, +GMT."""
    args = list(args)
    gmt = bool(args) and isinstance(args[-1], str) and args[-1].upper() == "GMT"
    if gmt:
        args = args[:-1]
    args = [int(a) for a in args]
    now = _datetime.datetime.now(_datetime.timezone.utc) if gmt else _datetime.datetime.now()

    def in_range(start, end, value):
        return start <= value <= end if start <= end else (value >= start or value <= end)

    if len(args) == 1:
        return now.hour == args[0]
    if len(args) == 2:
        return in_range(args[0], args[1], now.hour)
    if len(args) == 4:
        h1, m1, h2, m2 = args
        return in_range((h1, m1), (h2, m2), (now.hour, now.minute))
    if len(args) == 6:
        h1, m1, s1, h2, m2, s2 = args
        return in_range((h1, m1, s1), (h2, m2, s2), (now.hour, now.minute, now.second))
    return False

weekdayRange(wd1, /, *args) staticmethod

weekdayRange(wd1: _WEEKDAY, gmt: 'None|_Literal["GMT"]' = None) -> bool
weekdayRange(wd1: _WEEKDAY, wd2: _WEEKDAY, gmt: 'None|_Literal["GMT"]' = None) -> bool
Source code in src/proxylib/pac/__init__.py
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
@staticmethod
def weekdayRange(wd1: _WEEKDAY, /, *args: '_WEEKDAY|_Literal["GMT"]') -> bool:
    args = list(args)
    gmt = bool(args) and args[-1].upper() == "GMT"
    if gmt:
        args = args[:-1]
    wd2 = args[0] if args else wd1

    start = _WEEKDAYS.index(wd1.upper())
    end = _WEEKDAYS.index(wd2.upper())
    now = _datetime.datetime.now(_datetime.timezone.utc) if gmt else _datetime.datetime.now()
    today = now.isoweekday() % 7

    if start <= end:
        return start <= today <= end
    return today >= start or today <= end

clear_dns_cache()

Source code in src/proxylib/pac/__init__.py
60
61
def clear_dns_cache() -> None:
    _dns_cache.clear()

clear_download_cache()

Source code in src/proxylib/pac/__init__.py
369
370
def clear_download_cache() -> None:
    _download_cache.clear()

load(url, cache_ttl=300.0, **urllib_kwds)

Load a PAC script from a URL, a file: path, or inline JS source.

Requires the dukpy extra (proxylib[jspac]) to actually execute the script; without it, a warning is issued and an always-DIRECT :class:PAC is returned instead.

Genuine network downloads (not file: paths or inline JS) are cached per URL for cache_ttl seconds (default 5 minutes); pass cache_ttl=0/None to force a fresh fetch.

Source code in src/proxylib/pac/__init__.py
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
def load(url: str, cache_ttl: "float|None" = 300.0, **urllib_kwds) -> PAC:
    """Load a PAC script from a URL, a ``file:`` path, or inline JS source.

    Requires the ``dukpy`` extra (``proxylib[jspac]``) to actually execute
    the script; without it, a warning is issued and an always-DIRECT
    :class:`PAC` is returned instead.

    Genuine network downloads (not ``file:`` paths or inline JS) are cached
    per URL for ``cache_ttl`` seconds (default 5 minutes); pass
    ``cache_ttl=0``/``None`` to force a fresh fetch.
    """
    js = None
    from_network = False
    if "FindProxyForURL(" in url:
        js = url
    elif "://" not in url:
        if url.startswith("file:"):
            js = Path(url.removeprefix("file:")).read_text()
        else:
            url = "https://" + url

    if js is None:
        from_network = True
        if cache_ttl:
            cached = _download_cache.get(url)
            if cached is not None and (_time.monotonic() - cached[0]) < cache_ttl:
                return cached[1]
        with _urlopen(url, **urllib_kwds) as resp:
            js = _cast(bytes, resp.read()).decode()

    if "FindProxyForURL" not in js:
        raise ValueError(f"No FindProxyForURL found in response from: {url}")
    if not _jspac:
        _warn(f"Cannot load js from: {url} as pac. Install proxylib[jspac]")
        result = PAC()
    else:
        result = JSProxyAutoConfig(js)

    if from_network and cache_ttl:
        _download_cache[url] = (_time.monotonic(), result)
    return result

proxylib.pac.wpad

DNS + HTTP WPAD (Web Proxy Auto-Discovery) fallback, used by auto_proxy() when no explicit OS/env proxy configuration is found.

DHCP option 252 discovery is intentionally not implemented: it needs raw access to the OS's DHCP lease data, which has no portable stdlib-only path.

__all__ = ('discover',) module-attribute

discover(fqdn=None, timeout=3.0, cache_ttl=300.0, **urllib_kwds)

Try each http://wpad.<domain>/wpad.dat from most to least specific.

Returns the first successfully loaded PAC, or None if discovery fails. Results (including failures) are cached per fqdn for cache_ttl seconds; pass cache_ttl=0 (or None) to force a fresh probe.

Source code in src/proxylib/pac/wpad.py
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
def discover(
    fqdn: "Optional[str]" = None,
    timeout: float = 3.0,
    cache_ttl: "float|None" = 300.0,
    **urllib_kwds,
) -> "Optional[PAC]":
    """Try each ``http://wpad.<domain>/wpad.dat`` from most to least specific.

    Returns the first successfully loaded PAC, or None if discovery fails.
    Results (including failures) are cached per fqdn for ``cache_ttl``
    seconds; pass ``cache_ttl=0`` (or ``None``) to force a fresh probe.
    """
    fqdn = fqdn or socket.getfqdn()
    if cache_ttl:
        cached = _cache.get(fqdn)
        if cached is not None and (time.monotonic() - cached[0]) < cache_ttl:
            return cached[1]
    result = _discover(fqdn, timeout, **urllib_kwds)
    if cache_ttl:
        _cache[fqdn] = (time.monotonic(), result)
    return result

proxylib.pac.javascript

Runs a PAC script as real JavaScript via a pluggable engine (dukpy or quickjs -- see :mod:proxylib.pac.engines), exposing every PAC static/class method (and any subclass adds) into the JS global scope so FindProxyForURL can call them.

__all__ = ['JSContext'] module-attribute

JSContext(js, overrides=None)

Base class that boots a JS engine with _JSCONTEXT exported, then evals js.

The engine itself is pluggable (see :mod:proxylib.pac.engines) -- this class only depends on the small :class:~proxylib.pac.engines.base.JSEngine interface (export_function/eval/call), not on any specific engine's API.

Source code in src/proxylib/pac/javascript.py
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
def __init__(self, js: str, overrides: "Optional[Dict[str, Callable]]" = None) -> None:
    context: dict = object.__getattribute__(self, "_JSCONTEXT")
    if overrides:
        context = dict(context)
        # Wrapped as staticmethod so the binding loop below treats them
        # exactly like the class's own PAC utility functions (unwrapped
        # via val.__func__, no `self` bound) -- overrides are plain
        # standalone callables (e.g. `lambda host: "10.0.0.1"`), not
        # instance methods, so val.__get__(self) below would wrongly
        # bind `self` as an implicit first argument otherwise.
        context.update({key: staticmethod(fn) for key, fn in overrides.items()})
        # Stored per-instance, not mutated on the shared class-level
        # _JSCONTEXT, so overrides from one instance don't leak into
        # another's -- __getattribute__ below picks this up too, so an
        # override key that isn't one of the base PAC methods is still
        # recognized as exported.
        object.__setattr__(self, "_JSCONTEXT", context)
    engine_cls = get_engine_class()
    if engine_cls is None:
        raise ImportError(
            "No PAC JS engine is installed -- install one of the optional "
            "extras: proxylib[jspac] (dukpy) or proxylib[quickjs]."
        )
    engine = engine_cls()
    for key, val in context.items():
        if isinstance(val, staticmethod):
            val = val.__func__
        elif isinstance(val, classmethod):
            val = val.__get__(self.__class__)
        elif isinstance(val, property):
            raise NotImplementedError("JSContext does not support property exports yet")
        else:
            val = val.__get__(self)

        engine.export_function(key, val)
    engine.eval(js)

    # Pre-bind one callable per exported name now, instead of allocating
    # a fresh closure on every attribute access. Each one still calls
    # engine.call(key, ...), which resolves `key` fresh in the JS
    # engine's *global scope* at call time (not at bind time) -- that's
    # what actually implements "JS override wins": if `js` redefined
    # `key`, the engine resolves to that redefinition regardless of
    # when this Python-side wrapper was created. A naive
    # `object.__setattr__(self, key, jsFunction)` here would never be
    # reached, though -- __getattribute__ below checks membership in
    # `context` *before* ever falling through to instance-attribute
    # lookup, so the bound callables live in their own dict instead.
    bound: "Dict[str, object]" = {}
    for key in context:

        def _make_js_function(key=key):
            def jsFunction(*args):
                return engine.call(key, *args)

            return jsFunction

        bound[key] = _make_js_function()

    object.__setattr__(self, "_jsengine", engine)
    object.__setattr__(self, "_bound_js_functions", bound)

__getattribute__(name)

Source code in src/proxylib/pac/javascript.py
135
136
137
138
139
140
141
def __getattribute__(self, name: str):
    context: dict = object.__getattribute__(self, "_JSCONTEXT")
    if name in context:
        bound: dict = object.__getattribute__(self, "_bound_js_functions")
        return bound[name]
    else:
        return object.__getattribute__(self, name)

JSContextMeta

Bases: ABCMeta

Collects every alpha-leading attribute (methods) of a class and its bases into _JSCONTEXT, the set of functions exported into the JS engine.

__new__(metaclass, cls_name, base_classes, cls_builder)

Source code in src/proxylib/pac/javascript.py
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
def __new__(
    metaclass: "type[JSContext]",
    cls_name: str,
    base_classes: Sequence[object],
    cls_builder: "OrderedDict[str, object]",
):
    jsContext: "Dict[str, object]" = cls_builder.pop("_JSCONTEXT", {})
    exclude: "List[str]" = cls_builder.get("_JSCONTEXT_EXCLUDE", [])
    for key, val in cls_builder.items():
        if key[0].isalpha():
            jsContext.setdefault(key, val)

    for cls in reversed(base_classes):
        exclude.extend(getattr(cls, "_JSCONTEXT_EXCLUDE", []))
        if hasattr(cls, "_JSCONTEXT"):
            update: dict = cls._JSCONTEXT
        else:
            update = {
                key: getattr(cls, key) for key in dir(cls) if key[0].isalpha()
            }
        for key, val in update.items():
            jsContext.setdefault(key, staticmethod(val))

    return type.__new__(
        metaclass,
        cls_name,
        base_classes,
        {
            **{
                key: val
                for key, val in cls_builder.items()
                if not (key[0].isalpha() and key not in exclude)
            },
            "_JSCONTEXT": {
                key: val
                for key, val in jsContext.items()
                if (key[0].isalpha() and key not in exclude)
            },
        },
    )

proxylib.pac.engines.base

Common interface a PAC JS execution engine must implement.

:mod:proxylib.pac.javascript's JSContext is written against this protocol, not any specific engine -- dukpy.py/quickjs.py each wrap a concrete engine behind it.

__all__ = ('JSEngine',) module-attribute

JSEngine

Bases: Protocol

One JS execution context.

call(name, *args)

Call the JS global function name with args.

Must resolve name fresh in the engine's global scope on every call, not once at bind time -- this is what implements "a PAC script that redefines an exported name overrides it": the name is looked up again after the script has had a chance to reassign it.

Source code in src/proxylib/pac/engines/base.py
27
28
29
30
31
32
33
34
35
def call(self, name: str, *args: Any) -> Any:
    """Call the JS global function ``name`` with ``args``.

    Must resolve ``name`` fresh in the engine's global scope on every
    call, not once at bind time -- this is what implements "a PAC
    script that redefines an exported name overrides it": the name is
    looked up again after the script has had a chance to reassign it.
    """
    ...

eval(code)

Evaluate a script (used once, to load the PAC source).

Source code in src/proxylib/pac/engines/base.py
23
24
25
def eval(self, code: str) -> Any:
    """Evaluate a script (used once, to load the PAC source)."""
    ...

export_function(name, func)

Make func callable from JS as the global function name.

Source code in src/proxylib/pac/engines/base.py
19
20
21
def export_function(self, name: str, func: "Callable[..., Any]") -> None:
    """Make ``func`` callable from JS as the global function ``name``."""
    ...