Skip to content

Commit 9f52f58

Browse files
Merge pull request #274 from Distributive-Network/wes/DCP-4054/make-dcp-client-grok-pythonmonkey
dcp-client compatibility
2 parents e9195bc + 1d13bff commit 9f52f58

File tree

8 files changed

+201
-59
lines changed

8 files changed

+201
-59
lines changed

CMakeLists.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME)
4444
include(FetchContent)
4545

4646
SET(COMPILE_FLAGS "-ggdb -Ofast -fno-rtti") # optimize but also emit debug symbols
47-
SET( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${COMPILE_FLAGS}" )
47+
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${COMPILE_FLAGS} $ENV{EXTRA_CMAKE_CXX_FLAGS}")
4848

4949
set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake/modules)
5050
if(APPLE)

Makefile

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# @file Makefile
2+
# Not part of the PythonMonkey build - just workflow helper for Wes.
3+
# @author Wes Garland, [email protected]
4+
# @date March 2024
5+
#
6+
7+
BUILD = debug
8+
PYTHON = python3
9+
RUN = poetry run
10+
11+
PYTHON_BUILD_ENV = VERBOSE=1 EXTRA_CMAKE_CXX_FLAGS="$(EXTRA_CMAKE_CXX_FLAGS)"
12+
OS_NAME := $(shell uname -s)
13+
14+
ifeq ($(OS_NAME),Linux)
15+
CPU_COUNT=$(shell cat /proc/cpuinfo | grep -c processor)
16+
MAX_JOBS=10
17+
CPUS := $(shell test $(CPU_COUNT) -lt $(MAX_JOBS) && echo $(CPU_COUNT) || echo $(MAX_JOBS))
18+
PYTHON_BUILD_ENV += CPUS=$(CPUS)
19+
endif
20+
21+
EXTRA_CMAKE_CXX_FLAGS = -Wno-invalid-offsetof $(JOBS)
22+
23+
ifeq ($(BUILD),debug)
24+
EXTRA_CMAKE_CXX_FLAGS += -O0
25+
endif
26+
27+
.PHONY: build test all clean debug
28+
build:
29+
$(PYTHON_BUILD_ENV) $(PYTHON) ./build.py
30+
31+
test:
32+
$(RUN) ./peter-jr tests
33+
$(RUN) pytest tests/python
34+
35+
all: build test
36+
37+
clean:
38+
rm -rf build/src/CMakeFiles/pythonmonkey.dir
39+
rm -f build/src/pythonmonkey.so
40+
rm -f python/pythonmonkey.so
41+
42+
debug:
43+
@echo EXTRA_CMAKE_CXX_FLAGS=$(EXTRA_CMAKE_CXX_FLAGS)
44+
@echo JOBS=$(JOBS)
45+
@echo CPU_COUNT=$(CPU_COUNT)
46+
@echo OS_NAME=$(OS_NAME)

build.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
BUILD_DIR = os.path.join(TOP_DIR, "build")
1515

1616
# Get number of CPU cores
17-
CPUS = os.cpu_count() or 1
17+
CPUS = os.getenv('CPUS') or os.cpu_count() or 1
1818

1919
def execute(cmd: str, cwd: Optional[str] = None):
2020
popen = subprocess.Popen(cmd, stdout = subprocess.PIPE, stderr = subprocess.STDOUT,

python/pythonmonkey/builtin_modules/XMLHttpRequest-internal.py

+9-3
Original file line numberDiff line numberDiff line change
@@ -42,36 +42,44 @@ async def request(
4242
onNetworkError: Callable[[aiohttp.ClientError], None],
4343
/
4444
):
45+
debug = pm.bootstrap.require("debug");
46+
4547
class BytesPayloadWithProgress(aiohttp.BytesPayload):
4648
_chunkMaxLength = 2**16 # aiohttp default
4749

4850
async def write(self, writer) -> None:
51+
debug('xhr:io')('begin chunked write')
4952
buf = io.BytesIO(self._value)
5053
chunk = buf.read(self._chunkMaxLength)
5154
while chunk:
55+
debug('xhr:io')(' writing', len(chunk), 'bytes')
5256
await writer.write(chunk)
5357
processRequestBodyChunkLength(len(chunk))
5458
chunk = buf.read(self._chunkMaxLength)
5559
processRequestEndOfBody()
60+
debug('xhr:io')('finish chunked write')
5661

5762
if isinstance(body, str):
5863
body = bytes(body, "utf-8")
5964

6065
# set default headers
6166
headers.setdefault("user-agent", f"Python/{platform.python_version()} PythonMonkey/{pm.__version__}")
67+
debug('xhr:headers')('after set default\n', headers)
6268

6369
if timeoutMs > 0:
6470
timeoutOptions = aiohttp.ClientTimeout(total=timeoutMs/1000) # convert to seconds
6571
else:
6672
timeoutOptions = aiohttp.ClientTimeout() # default timeout
6773

6874
try:
75+
debug('xhr:aiohttp')('creating request for', url)
6976
async with aiohttp.request(method=method,
7077
url=yarl.URL(url, encoded=True),
7178
headers=headers,
7279
data=BytesPayloadWithProgress(body) if body else None,
7380
timeout=timeoutOptions,
7481
) as res:
82+
debug('xhr:aiohttp')('got', res.content_type, 'result')
7583
def getResponseHeader(name: str):
7684
return res.headers.get(name)
7785
def getAllResponseHeaders():
@@ -81,6 +89,7 @@ def getAllResponseHeaders():
8189
headers.sort()
8290
return "\r\n".join(headers)
8391
def abort():
92+
debug('xhr:io')('abort')
8493
res.close()
8594

8695
# readyState HEADERS_RECEIVED
@@ -92,15 +101,12 @@ def abort():
92101
'getResponseHeader': getResponseHeader,
93102
'getAllResponseHeaders': getAllResponseHeaders,
94103
'abort': abort,
95-
96104
'contentLength': res.content_length or 0,
97105
}
98106
processResponse(responseData)
99107

100-
# readyState LOADING
101108
async for data in res.content.iter_any():
102109
processBodyChunk(bytearray(data)) # PythonMonkey only accepts the mutable bytearray type
103-
104110
# readyState DONE
105111
processEndOfBody()
106112
except asyncio.TimeoutError as e:

python/pythonmonkey/builtin_modules/XMLHttpRequest.js

+50-5
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,39 @@
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+
/**
19+
* Truncate a string-like thing for display purposes, returning a string.
20+
* @param {any} what The thing to truncate; must have a slice method and index property.
21+
* Works with string, array, typedarray, etc.
22+
* @param {number} maxlen The maximum length for truncation
23+
* @param {boolean} coerce Not false = coerce to printable character codes
24+
* @returns {string}
25+
*/
26+
function trunc(what, maxlen, coerce)
27+
{
28+
if (coerce !== false && typeof what !== 'string')
29+
{
30+
what = Array.from(what).map(x => {
31+
if (x > 31 && x < 127)
32+
return String.fromCharCode(x);
33+
else if (x < 32)
34+
return String.fromCharCode(0x2400 + Number(x));
35+
else if (x === 127)
36+
return '\u2421';
37+
else
38+
return '\u2423';
39+
}).join('');
40+
}
41+
return `${what.slice(0, maxlen)}${what.length > maxlen ? '\u2026' : ''}`;
42+
}
1543

1644
// exposed
1745
/**
@@ -29,6 +57,7 @@ class ProgressEvent extends Event
2957
this.lengthComputable = eventInitDict.lengthComputable ?? false;
3058
this.loaded = eventInitDict.loaded ?? 0;
3159
this.total = eventInitDict.total ?? 0;
60+
this.debugTag = 'xhr:';
3261
}
3362
}
3463

@@ -112,6 +141,7 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
112141
*/
113142
open(method, url, async = true, username = null, password = null)
114143
{
144+
debug('xhr:open')('open start, method=' + method);
115145
// Normalize the method.
116146
// @ts-expect-error
117147
method = method.toString().toUpperCase();
@@ -125,7 +155,8 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
125155
parsedURL.username = username;
126156
if (password)
127157
parsedURL.password = password;
128-
158+
debug('xhr:open')('url is ' + parsedURL.href);
159+
129160
// step 11
130161
this.#sendFlag = false;
131162
this.#uploadListenerFlag = false;
@@ -144,6 +175,7 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
144175
this.#state = XMLHttpRequest.OPENED;
145176
this.dispatchEvent(new Event('readystatechange'));
146177
}
178+
debug('xhr:open')('finished open, state is ' + this.#state);
147179
}
148180

149181
/**
@@ -153,6 +185,7 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
153185
*/
154186
setRequestHeader(name, value)
155187
{
188+
debug('xhr:headers')(`set header ${name}=${value}`);
156189
if (this.#state !== XMLHttpRequest.OPENED)
157190
throw new DOMException('setRequestHeader can only be called when state is OPEN', 'InvalidStateError');
158191
if (this.#sendFlag)
@@ -218,6 +251,7 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
218251
*/
219252
send(body = null)
220253
{
254+
debug('xhr:send')(`sending; body length=${body?.length}`);
221255
if (this.#state !== XMLHttpRequest.OPENED) // step 1
222256
throw new DOMException('connection must be opened before send() is called', 'InvalidStateError');
223257
if (this.#sendFlag) // step 2
@@ -248,10 +282,9 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
248282

249283
const originalAuthorContentType = this.#requestHeaders['content-type'];
250284
if (!originalAuthorContentType && extractedContentType)
251-
{
252285
this.#requestHeaders['content-type'] = extractedContentType;
253-
}
254286
}
287+
debug('xhr:send')(`content-type=${this.#requestHeaders['content-type']}`);
255288

256289
// step 5
257290
if (this.#uploadObject._hasAnyListeners())
@@ -276,6 +309,7 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
276309
*/
277310
#sendAsync()
278311
{
312+
debug('xhr:send')('sending in async mode');
279313
this.dispatchEvent(new ProgressEvent('loadstart', { loaded:0, total:0 })); // step 11.1
280314

281315
let requestBodyTransmitted = 0; // step 11.2
@@ -308,6 +342,7 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
308342
let responseLength = 0;
309343
const processResponse = (response) =>
310344
{
345+
debug('xhr:response')(`response headers ----\n${response.getAllResponseHeaders()}`);
311346
this.#response = response; // step 11.9.1
312347
this.#state = XMLHttpRequest.HEADERS_RECEIVED; // step 11.9.4
313348
this.dispatchEvent(new Event('readystatechange')); // step 11.9.5
@@ -318,6 +353,7 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
318353

319354
const processBodyChunk = (/** @type {Uint8Array} */ bytes) =>
320355
{
356+
debug('xhr:response')(`recv chunk, ${bytes.length} bytes (${trunc(bytes, 100)})`);
321357
this.#receivedBytes.push(bytes);
322358
if (this.#state === XMLHttpRequest.HEADERS_RECEIVED)
323359
this.#state = XMLHttpRequest.LOADING;
@@ -330,16 +366,22 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
330366
*/
331367
const processEndOfBody = () =>
332368
{
369+
debug('xhr:response')(`end of body, received ${this.#receivedLength} bytes`);
333370
const transmitted = this.#receivedLength; // step 3
334371
const length = responseLength || 0; // step 4
372+
335373
this.dispatchEvent(new ProgressEvent('progress', { loaded:transmitted, total:length })); // step 6
336374
this.#state = XMLHttpRequest.DONE; // step 7
337375
this.#sendFlag = false; // step 8
376+
338377
this.dispatchEvent(new Event('readystatechange')); // step 9
339378
for (const eventType of ['load', 'loadend']) // step 10, step 11
340379
this.dispatchEvent(new ProgressEvent(eventType, { loaded:transmitted, total:length }));
341380
};
342381

382+
debug('xhr:send')(`${this.#requestMethod} ${this.#requestURL.href}`);
383+
debug('xhr:headers')('headers=' + Object.entries(this.#requestHeaders));
384+
343385
// send() step 6
344386
request(
345387
this.#requestMethod,
@@ -362,8 +404,8 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
362404
*/
363405
#sendSync()
364406
{
407+
/* Synchronous XHR deprecated. /wg march 2024 */
365408
throw new DOMException('synchronous XHR is not supported', 'NotSupportedError');
366-
// TODO: handle synchronous request
367409
}
368410

369411
/**
@@ -376,7 +418,6 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
376418
return;
377419
if (this.#timedOutFlag) // step 2
378420
return this.#reportRequestError('timeout', new DOMException(e.toString(), 'TimeoutError'));
379-
console.error(e); // similar to browsers, print out network errors even then the error will be handled by `xhr.onerror`
380421
if (this.#response === null /* network error */) // step 4
381422
return this.#reportRequestError('error', new DOMException(e.toString(), 'NetworkError'));
382423
else // unknown errors
@@ -652,6 +693,10 @@ class XMLHttpRequest extends XMLHttpRequestEventTarget
652693
}
653694
}
654695

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

0 commit comments

Comments
 (0)