Skip to content

Commit 9241d10

Browse files
committed
Add advanced debug output via debug module to XHR and event emitter built-ins
1 parent 4e065b1 commit 9241d10

4 files changed

Lines changed: 85 additions & 84 deletions

File tree

python/pythonmonkey/builtin_modules/XMLHttpRequest-internal.py

Lines changed: 11 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
import platform
1212
import pythonmonkey as pm
1313
from typing import Union, ByteString, Callable, TypedDict
14-
import traceback
1514

1615
class XHRResponse(TypedDict, total=True):
1716
"""
@@ -43,79 +42,57 @@ async def request(
4342
onNetworkError: Callable[[aiohttp.ClientError], None],
4443
/
4544
):
46-
#print("request START about to crash, calling gc");
47-
#pm.collect()
48-
49-
#print("request START print stack");
50-
#traceback.print_stack()
51-
52-
#print("request START access headers");
53-
#print("headers['accept'] is ", headers['accept']);
54-
#print("headers['x-dcp-platform'] is ", headers['x-dcp-platform']);
55-
#print("request START, headers are ", headers);
56-
45+
debug = pm.bootstrap.require("debug");
46+
5747
class BytesPayloadWithProgress(aiohttp.BytesPayload):
5848
_chunkMaxLength = 2**16 # aiohttp default
5949

6050
async def write(self, writer) -> None:
61-
print("write START");
51+
debug('xhr:io')('begin chunked write')
6252
buf = io.BytesIO(self._value)
6353
chunk = buf.read(self._chunkMaxLength)
6454
while chunk:
55+
debug('xhr:io')(' writing', len(chunk), 'bytes')
6556
await writer.write(chunk)
6657
processRequestBodyChunkLength(len(chunk))
6758
chunk = buf.read(self._chunkMaxLength)
6859
processRequestEndOfBody()
69-
print("write END");
60+
debug('xhr:io')('finish chunked write')
7061

7162
if isinstance(body, str):
7263
body = bytes(body, "utf-8")
7364

7465
# set default headers
75-
#print("headers=dict(headers) BEFORE setdefault");
7666
headers.setdefault("user-agent", f"Python/{platform.python_version()} PythonMonkey/{pm.__version__}")
77-
print("HEADERS after setdefault: ", headers)
78-
79-
80-
81-
print("METHOD is ", method)
82-
print("URL is ", url)
83-
print("TIMEOUT is ", timeoutMs)
84-
#print("BODY is ", body)
85-
67+
debug('xhr:headers')('after set default\n', headers)
8668

8769
if timeoutMs > 0:
8870
timeoutOptions = aiohttp.ClientTimeout(total=timeoutMs/1000) # convert to seconds
8971
else:
9072
timeoutOptions = aiohttp.ClientTimeout() # default timeout
9173

9274
try:
93-
print("async with aiohttp.request BEFORE");
75+
debug('xhr:aiohttp')('creating request for', url)
9476
async with aiohttp.request(method=method,
9577
url=yarl.URL(url, encoded=True),
9678
headers=headers,
9779
data=BytesPayloadWithProgress(body) if body else None,
9880
timeout=timeoutOptions,
9981
) as res:
100-
print("async with aiohttp.request AFTER");
82+
debug('xhr:aiohttp')('got', res.content_type, 'result')
10183
def getResponseHeader(name: str):
102-
print("getAllResponseHeader");
10384
return res.headers.get(name)
10485
def getAllResponseHeaders():
10586
headers = []
106-
print("getAllResponseHeaders BEFORE");
10787
for name, value in res.headers.items():
10888
headers.append(f"{name.lower()}: {value}")
109-
print("getAllResponseHeaders AFTER");
11089
headers.sort()
111-
print("getAllResponseHeaders sorted");
11290
return "\r\n".join(headers)
11391
def abort():
114-
print("abort");
92+
debug('xhr:io')('abort')
11593
res.close()
11694

11795
# readyState HEADERS_RECEIVED
118-
print(res.headers)
11996
responseData: XHRResponse = { # FIXME: PythonMonkey bug: the dict will be GCed if directly as an argument
12097
'url': str(res.real_url),
12198
'status': res.status,
@@ -124,31 +101,24 @@ def abort():
124101
'getResponseHeader': getResponseHeader,
125102
'getAllResponseHeaders': getAllResponseHeaders,
126103
'abort': abort,
127-
104+
128105
'contentLength': res.content_length or 0,
129106
}
130-
print("processResponse");
131107
processResponse(responseData)
132108

133-
# readyState LOADING
134-
print("readyState LOADING");
135109
async for data in res.content.iter_any():
136110
processBodyChunk(bytearray(data)) # PythonMonkey only accepts the mutable bytearray type
137-
print("readyState DONE");
138-
111+
139112
# readyState DONE
140113
processEndOfBody()
141114
except asyncio.TimeoutError as e:
142-
print("onTimeoutError " + e);
143115
onTimeoutError(e)
144116
raise # rethrow
145117
except aiohttp.ClientError as e:
146-
print("onNetworkError " + e);
147118
onNetworkError(e)
148119
raise # rethrow
149120

150121
def decodeStr(data: bytes, encoding='utf-8'): # XXX: Remove this once we get proper TextDecoder support
151-
print("decodeStr")
152122
return str(data, encoding=encoding)
153123

154124
# Module exports

python/pythonmonkey/builtin_modules/XMLHttpRequest.js

Lines changed: 51 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,41 @@
77
*
88
* @copyright Copyright (c) 2023 Distributive Corp.
99
*/
10+
'use strict';
1011

1112
const { EventTarget, Event } = require('event-target');
1213
const { DOMException } = require('dom-exception');
1314
const { URL, URLSearchParams } = require('url');
1415
const { request, decodeStr } = require('XMLHttpRequest-internal');
16+
const debug = globalThis.python.eval('__import__("pythonmonkey").bootstrap.require')('debug');
17+
18+
debug('xhr:module')('loaded XMLHttpRequest.js module');
19+
20+
/**
21+
* Truncate a string-like thing for display purposes, returning a string.
22+
* @param {any} what The thing to truncate; must have a slice method and index property.
23+
* Works with string, array, typedarray, etc.
24+
* @param {number} maxlen The maximum length for truncation
25+
* @param {boolean} coerce Not false = coerce to printable character codes
26+
* @returns {string}
27+
*/
28+
function trunc(what, maxlen, coerce)
29+
{
30+
if (coerce !== false && typeof what !== 'string')
31+
{
32+
what = Array.from(what).map(x => {
33+
if (x > 31 && x < 127)
34+
return String.fromCharCode(x);
35+
else if (x < 32)
36+
return eval('"\\u24' + ((x).toString(16)).padStart(2,0) + '"')
37+
else if (x === 127)
38+
return '\u2421';
39+
else
40+
return '\u2423';
41+
}).join('');
42+
}
43+
return `${what.slice(0, maxlen)}${what.length > maxlen ? '\u2026' : ''}`;
44+
}
1545

1646
// exposed
1747
/**
@@ -29,6 +59,7 @@ class ProgressEvent extends Event
2959
this.lengthComputable = eventInitDict.lengthComputable ?? false;
3060
this.loaded = eventInitDict.loaded ?? 0;
3161
this.total = eventInitDict.total ?? 0;
62+
this.debugTag = 'xhr:';
3263
}
3364
}
3465

@@ -112,26 +143,22 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
112143
*/
113144
open(method, url, async = true, username = null, password = null)
114145
{
115-
console.log('OPEN START');
146+
debug('xhr:open')('open start, method=' + method);
116147
// Normalize the method.
117148
// @ts-expect-error
118149
method = method.toString().toUpperCase();
119150

120-
console.log('OPEN METHOD is ' + method);
121-
122151
// Check for valid request method
123152
if (!method || FORBIDDEN_REQUEST_METHODS.includes(method))
124153
throw new DOMException('Request method not allowed', 'SecurityError');
125154

126155
const parsedURL = new URL(url);
127-
// parsedURL.protocol = 'http:';
128156
if (username)
129157
parsedURL.username = username;
130158
if (password)
131159
parsedURL.password = password;
160+
debug('xhr:open')('url is ' + parsedURL.href);
132161

133-
console.log('OPEN URL is ' + parsedURL);
134-
135162
// step 11
136163
this.#sendFlag = false;
137164
this.#uploadListenerFlag = false;
@@ -140,7 +167,6 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
140167
if (async === false)
141168
this.#synchronousFlag = true;
142169
this.#requestHeaders = {}; // clear
143-
this.setRequestHeader('Accept-Encoding', 'identity');
144170
this.#response = null;
145171
this.#receivedBytes = [];
146172
this.#responseObject = null;
@@ -151,7 +177,7 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
151177
this.#state = XMLHttpRequest.OPENED;
152178
this.dispatchEvent(new Event('readystatechange'));
153179
}
154-
console.log('OPEN END');
180+
debug('xhr:open')('finished open, state is ' + this.#state);
155181
}
156182

157183
/**
@@ -161,8 +187,7 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
161187
*/
162188
setRequestHeader(name, value)
163189
{
164-
console.log('setRequestHeader, name ' + name + ' value ' + value);
165-
//console.log(new Error().stack);
190+
debug('xhr:headers')(`set header ${name}=${value}`);
166191
if (this.#state !== XMLHttpRequest.OPENED)
167192
throw new DOMException('setRequestHeader can only be called when state is OPEN', 'InvalidStateError');
168193
if (this.#sendFlag)
@@ -228,7 +253,7 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
228253
*/
229254
send(body = null)
230255
{
231-
console.log('SEND');
256+
debug('xhr:send')(`sending; body length=${body?.length}`);
232257
if (this.#state !== XMLHttpRequest.OPENED) // step 1
233258
throw new DOMException('connection must be opened before send() is called', 'InvalidStateError');
234259
if (this.#sendFlag) // step 2
@@ -259,11 +284,9 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
259284

260285
const originalAuthorContentType = this.#requestHeaders['content-type'];
261286
if (!originalAuthorContentType && extractedContentType)
262-
{
263-
console.log('CONTENT_TYPE');
264287
this.#requestHeaders['content-type'] = extractedContentType;
265-
}
266288
}
289+
debug('xhr:send')(`content-type=${this.#requestHeaders['content-type']}`);
267290

268291
// step 5
269292
if (this.#uploadObject._hasAnyListeners())
@@ -288,7 +311,7 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
288311
*/
289312
#sendAsync()
290313
{
291-
console.log('SEND ASYNC');
314+
debug('xhr:send')('sending in async mode');
292315
this.dispatchEvent(new ProgressEvent('loadstart', { loaded:0, total:0 })); // step 11.1
293316

294317
let requestBodyTransmitted = 0; // step 11.2
@@ -321,59 +344,46 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
321344
let responseLength = 0;
322345
const processResponse = (response) =>
323346
{
324-
console.log('processResponse 1, response is ' + response.toString());
325-
console.log('processResponse 1, response header is ' + response.getAllResponseHeaders());
326-
for (var key in response){
327-
console.log( key + ": " + response[key]);
328-
}
347+
debug('xhr:response')(`response headers ----\n${response.getAllResponseHeaders()}`);
329348
this.#response = response; // step 11.9.1
330349
this.#state = XMLHttpRequest.HEADERS_RECEIVED; // step 11.9.4
331350
this.dispatchEvent(new Event('readystatechange')); // step 11.9.5
332351
if (this.#state !== XMLHttpRequest.HEADERS_RECEIVED) // step 11.9.6
333352
return;
334353
responseLength = this.#response.contentLength; // step 11.9.8
335-
console.log('processResponse 2, responseLength is ' + responseLength);
336354
};
337355

338356
const processBodyChunk = (/** @type {Uint8Array} */ bytes) =>
339357
{
340-
console.log('processBodyChunk 1, bytes are ' + bytes);
358+
debug('xhr:response')(`recv chunk, ${bytes.length} bytes (${trunc(bytes, 100)})`);
341359
this.#receivedBytes.push(bytes);
342360
if (this.#state === XMLHttpRequest.HEADERS_RECEIVED)
343361
this.#state = XMLHttpRequest.LOADING;
344362
this.dispatchEvent(new Event('readystatechange'));
345363
this.dispatchEvent(new ProgressEvent('progress', { loaded:this.#receivedLength, total:responseLength }));
346-
console.log('processBodyChunk 2');
347364
};
348365

349366
/**
350367
* @see https://xhr.spec.whatwg.org/#handle-response-end-of-body
351368
*/
352369
const processEndOfBody = () =>
353370
{
354-
console.log('processEndOfBody receivedLength is ' + this.#receivedLength);
371+
debug('xhr:response')(`end of body, received ${this.#receivedLength} bytes`);
355372
const transmitted = this.#receivedLength; // step 3
356373
const length = responseLength || 0; // step 4
357-
console.log('processEndOfBody responseLength is ' + responseLength);
374+
358375
this.dispatchEvent(new ProgressEvent('progress', { loaded:transmitted, total:length })); // step 6
359-
console.log('processEndOfBody AGAIN 1 responseLength is ' + responseLength);
360376
this.#state = XMLHttpRequest.DONE; // step 7
361-
console.log('processEndOfBody AGAIN 2 responseLength is ' + responseLength);
362377
this.#sendFlag = false; // step 8
363-
console.log('processEndOfBody AGAIN 3 responseLength is ' + responseLength);
378+
364379
this.dispatchEvent(new Event('readystatechange')); // step 9
365-
console.log('processEndOfBody AGAIN 4 responseLength is ' + responseLength);
366-
console.log('processEndOfBody before loop');
367380
for (const eventType of ['load', 'loadend']) // step 10, step 11
368381
this.dispatchEvent(new ProgressEvent(eventType, { loaded:transmitted, total:length }));
369-
console.log('processEndOfBody after loop');
370382
};
371383

372-
console.log('CALLING REQUEST');
373-
console.log('CALLING REQUEST, METHOD is ' + this.#requestMethod);
374-
console.log('CALLING REQUEST, URL is ' + this.#requestURL.toString());
375-
console.log('CALLING REQUEST, HEADERS are ' + this.#requestHeaders);
376-
console.log('CALLING REQUEST, TIMEOUT is ' + this.timeout);
384+
debug('xhr:send')(`${this.#requestMethod} ${this.#requestURL.href}`);
385+
debug('xhr:headers')('headers=' + Object.entries(this.#requestHeaders));
386+
377387
// send() step 6
378388
request(
379389
this.#requestMethod,
@@ -396,8 +406,8 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
396406
*/
397407
#sendSync()
398408
{
409+
/* Synchronous XHR deprecated. /wg march 2024 */
399410
throw new DOMException('synchronous XHR is not supported', 'NotSupportedError');
400-
// TODO: handle synchronous request
401411
}
402412

403413
/**
@@ -410,7 +420,6 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
410420
return;
411421
if (this.#timedOutFlag) // step 2
412422
return this.#reportRequestError('timeout', new DOMException(e.toString(), 'TimeoutError'));
413-
console.error(e); // similar to browsers, print out network errors even then the error will be handled by `xhr.onerror`
414423
if (this.#response === null /* network error */) // step 4
415424
return this.#reportRequestError('error', new DOMException(e.toString(), 'NetworkError'));
416425
else // unknown errors
@@ -686,6 +695,10 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
686695
}
687696
}
688697

698+
/* A side-effect of loading this module is to add the XMLHttpRequest and related symbols to the global
699+
* object. This makes them accessible in the "normal" way (like in a browser) even in PythonMonkey JS
700+
* host environments which don't include a require() symbol.
701+
*/
689702
if (!globalThis.XMLHttpRequestEventTarget)
690703
globalThis.XMLHttpRequestEventTarget = XMLHttpRequestEventTarget;
691704
if (!globalThis.XMLHttpRequestUpload)

python/pythonmonkey/builtin_modules/event-target.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
*
88
* @copyright Copyright (c) 2023 Distributive Corp.
99
*/
10+
'use strict';
11+
const debug = globalThis.python.eval('__import__("pythonmonkey").bootstrap.require')('debug');
1012

1113
/**
1214
* The Event interface represents an event which takes place in the DOM.
@@ -85,8 +87,6 @@ class Event
8587
{
8688
this.type = type;
8789
}
88-
89-
// TODO: to be implemented
9090
}
9191

9292
/**
@@ -136,6 +136,7 @@ class EventTarget
136136
*/
137137
dispatchEvent(event)
138138
{
139+
debug((event.debugTag || '') + 'events:dispatch')(event.constructor.name, event.type);
139140
// Set the Event.target property to the current EventTarget
140141
event.target = this;
141142

0 commit comments

Comments
 (0)