depyo 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -28,7 +28,12 @@ node depyo.js --skip-path /path/to/file.pyc
28
28
 
29
29
  # Dump to stdout instead of files
30
30
  node depyo.js --out /path/to/file.pyc
31
+
32
+ # Marshal-only blob (no .pyc header)
33
+ node depyo.js --marshal --py-version 3.11 /path/to/blob.bin
34
+ node depyo.js --marshal /path/to/blob.bin
31
35
  ```
36
+ Without `--py-version`, depyo scans supported versions (oldest → newest) and accepts the first clean output when all clean candidates agree. If outputs diverge (ambiguous), it stops and asks for `--py-version`. Use `--debug` to see scan results.
32
37
 
33
38
  ### CLI options
34
39
  - `--asm` emit `.pyasm` disassembly alongside source
@@ -39,6 +44,8 @@ node depyo.js --out /path/to/file.pyc
39
44
  - `--skip-source-gen` skip writing `.py` (use with `--asm/--dump`)
40
45
  - `--skip-path` flatten output paths (write next to input)
41
46
  - `--out` print source to stdout instead of files
47
+ - `--marshal` treat input as raw marshalled data (no .pyc header, auto-scan versions)
48
+ - `--py-version <x.y>` bytecode version hint (use with `--marshal`)
42
49
  - `--basedir <dir>` override output root (default: alongside input)
43
50
  - `--file-ext <ext>` change emitted extension (default `py`)
44
51
 
@@ -58,6 +65,14 @@ node depyo.js --out /path/to/file.pyc
58
65
  node scripts/run-matrix.js # full sweep
59
66
  node scripts/run-matrix.js --pattern py311_exception_groups --fail-fast
60
67
  ```
68
+ - Marshal fixtures (headerless marshal blobs):
69
+ ```bash
70
+ node scripts/run-marshal-fixtures.js
71
+ ```
72
+ - Regenerate marshal fixtures:
73
+ ```bash
74
+ node scripts/generate-marshal-fixtures.js --clean
75
+ ```
61
76
  - Modern fixtures are generated via `test/generate_modern_tests.py` (Python 3.8+ on PATH).
62
77
 
63
78
  ## Support matrix
package/depyo.js CHANGED
@@ -20,6 +20,9 @@ global.g_cliArgs = {
20
20
  skipSource: false,
21
21
  skipPath: false,
22
22
  sendToStdout: false,
23
+ marshal: false,
24
+ pyVersion: null,
25
+ silent: false,
23
26
  fileExt: 'py',
24
27
  baseDir: null,
25
28
  filenames: []
@@ -29,6 +32,7 @@ let g_totalInThroughput = 0;
29
32
  let g_totalOutThroughput = 0;
30
33
  let g_totalExecTime = 0;
31
34
  let g_totalFiles = 0;
35
+ let g_pyVersionInfo = null;
32
36
 
33
37
  function printUsage() {
34
38
  console.log(`Usage: node depyo.js [options] <file.pyc|archive.zip> [...]
@@ -43,6 +47,8 @@ Options:
43
47
  --skip-source-gen Do not emit .py source (useful with --asm/--dump)
44
48
  --skip-path Flatten output paths (write files next to inputs)
45
49
  --out Print decompiled source to stdout instead of files
50
+ --marshal Treat input as raw marshalled data (no .pyc header)
51
+ --py-version <x.y> Python bytecode version hint (auto-scan if omitted)
46
52
  --basedir <path> Output base directory (default: alongside input)
47
53
  --file-ext <ext> Extension for generated source (default: py)
48
54
  `);
@@ -69,6 +75,10 @@ function parseCLIParams() {
69
75
  g_cliArgs.skipPath = true;
70
76
  } else if (cliParam.toLowerCase() == "--out") {
71
77
  g_cliArgs.sendToStdout = true;
78
+ } else if (cliParam.toLowerCase() == "--marshal") {
79
+ g_cliArgs.marshal = true;
80
+ } else if (cliParam.toLowerCase() == "--py-version") {
81
+ g_cliArgs.pyVersion = process.argv[++idx];
72
82
  } else if (cliParam.toLowerCase() == "--basedir") {
73
83
  g_cliArgs.baseDir = process.argv[++idx];
74
84
  } else if (cliParam.toLowerCase() == "--file-ext") {
@@ -84,37 +94,176 @@ function parseCLIParams() {
84
94
  }
85
95
  }
86
96
 
97
+ function normalizeMarshalOutput(src) {
98
+ return src
99
+ .replace(/\r\n/g, '\n')
100
+ .replace(/[ \t]+$/gm, '')
101
+ .replace(/\n{3,}/g, '\n\n')
102
+ .replace(/#[^\n]*$/gm, '')
103
+ .trim();
104
+ }
105
+
106
+ function attemptMarshalDecompile(buffer, versionInfo, opts = {}) {
107
+ const prevSilent = g_cliArgs.silent;
108
+ const prevDebug = g_cliArgs.debug;
109
+ if (opts.silent) {
110
+ g_cliArgs.silent = true;
111
+ g_cliArgs.debug = false;
112
+ }
113
+ try {
114
+ const rdr = new PycReader(buffer, {marshal: true, versionInfo});
115
+ const obj = rdr.ReadObject();
116
+ if (!obj || obj.ClassName !== "Py_CodeObject") {
117
+ return null;
118
+ }
119
+ const opcodes = new versionInfo.opcode(obj);
120
+ const {unknown, total} = PycReader.CountUnknownOpcodes(obj, rdr, opcodes.OpCodeList);
121
+ const unknownRatio = total > 0 ? unknown / total : 1;
122
+ const remaining = rdr.m_rdr.Reader.length - rdr.m_rdr.pc;
123
+
124
+ const genStartTS = process.hrtime.bigint();
125
+ const decompiler = new PycDecompiler(obj);
126
+ const ast = decompiler.decompile();
127
+ const pycResult = ast.codeFragment();
128
+ const pySrc = pycResult.toString();
129
+ const genSecs = Number(process.hrtime.bigint() - genStartTS) / 1000000000;
130
+
131
+ return {
132
+ reader: rdr,
133
+ obj,
134
+ pySrc,
135
+ genSecs,
136
+ cleanBuild: decompiler.cleanBuild,
137
+ unknown,
138
+ total,
139
+ unknownRatio,
140
+ remaining,
141
+ versionInfo
142
+ };
143
+ } catch (ex) {
144
+ if (opts.debug) {
145
+ console.log(`Marshal decompile failed for ${versionInfo.major}.${versionInfo.minor}: ${ex.message}`);
146
+ }
147
+ return null;
148
+ } finally {
149
+ g_cliArgs.silent = prevSilent;
150
+ g_cliArgs.debug = prevDebug;
151
+ }
152
+ }
153
+
154
+ function selectMarshalCandidate(buffer) {
155
+ const candidates = PycReader.ListSupportedVersions(false);
156
+ const attempts = [];
157
+ const cleanCandidates = [];
158
+ let baselineOutput = null;
159
+ let outputsDiverged = false;
160
+ for (const candidate of candidates) {
161
+ const result = attemptMarshalDecompile(buffer, candidate, {silent: true});
162
+ if (!result) {
163
+ continue;
164
+ }
165
+ attempts.push(result);
166
+ const isWorking = result.cleanBuild && result.unknown === 0 && result.remaining === 0;
167
+ if (isWorking) {
168
+ const normalized = normalizeMarshalOutput(result.pySrc || '');
169
+ cleanCandidates.push({...result, normalized});
170
+ if (baselineOutput === null) {
171
+ baselineOutput = normalized;
172
+ } else if (baselineOutput !== normalized) {
173
+ outputsDiverged = true;
174
+ }
175
+ }
176
+ }
177
+
178
+ if (!cleanCandidates.length) {
179
+ return attempts.length ? {best: null, attempts, ambiguous: false} : null;
180
+ }
181
+
182
+ if (outputsDiverged) {
183
+ return {best: null, attempts, ambiguous: true, cleanCandidates};
184
+ }
185
+
186
+ return {best: cleanCandidates[0], attempts, ambiguous: false};
187
+ }
188
+
87
189
  function decompilePycObject(data) {
88
190
  try
89
191
  {
90
192
  let filename = null, obj = null;
91
193
  let startTS = process.hrtime.bigint();
92
- let rdr = new PycReader(data);
93
- try {
94
- obj = rdr.ReadObject();
95
- filename = g_baseDir + obj.FileName;
96
- } catch (ex) {
97
- if (ex instanceof PycReader.LoadError) {
98
- // Save the binary file if it not already exists for future manual analysis.
99
- if (!ex.FileName) {
100
- if (global.g_cliArgs?.debug) {
101
- console.log(`LoadError: ${ex.message} at position ${ex.position}`);
194
+ let buffer = data;
195
+ if (!Buffer.isBuffer(buffer)) {
196
+ buffer = fs.readFileSync(data);
197
+ }
198
+ let rdr = null;
199
+ let pySrc = null;
200
+ let genSecs = 0;
201
+
202
+ if (g_cliArgs.marshal) {
203
+ let attemptResult = null;
204
+ if (g_pyVersionInfo) {
205
+ attemptResult = attemptMarshalDecompile(buffer, g_pyVersionInfo);
206
+ } else {
207
+ const scan = selectMarshalCandidate(buffer);
208
+ attemptResult = scan ? scan.best : null;
209
+ if (g_cliArgs.debug && scan?.attempts?.length) {
210
+ console.log("Marshal scan results:");
211
+ for (const attempt of scan.attempts) {
212
+ const info = attempt.versionInfo;
213
+ console.log(` ${info.major}.${info.minor}: clean=${attempt.cleanBuild} unknown=${attempt.unknown}/${attempt.total} remaining=${attempt.remaining}`);
214
+ }
215
+ if (attemptResult) {
216
+ const info = attemptResult.versionInfo;
217
+ console.log(`Selected marshal version: ${info.major}.${info.minor}`);
102
218
  }
103
- return;
104
219
  }
105
- filename = g_baseDir + ex.FileName;
106
- let dirPath = Path.dirname(filename);
107
- if (!g_cliArgs.skipPath && !fs.existsSync(dirPath)) {
108
- fs.mkdirSync(dirPath, {recursive: true});
220
+ if (!attemptResult && scan?.ambiguous) {
221
+ const versions = scan.cleanCandidates
222
+ ? scan.cleanCandidates.map(c => `${c.versionInfo.major}.${c.versionInfo.minor}`).join(', ')
223
+ : 'unknown';
224
+ throw new Error(`Ambiguous marshal version (${versions}). Provide --py-version X.Y to force.`);
109
225
  }
110
- let filenamePyc = filename.substring(0, filename.lastIndexOf('.')) + ".pyc";
111
- fs.writeFileSync(filenamePyc, rdr.Reader);
112
- console.log(`Error: ${ex.message}\nFile: ${filenamePyc}\nPosition: ${ex.position}`);
226
+ }
227
+
228
+ if (!attemptResult) {
229
+ throw new Error("No clean marshal candidate found. Provide --py-version X.Y to force.");
230
+ }
231
+ rdr = attemptResult.reader;
232
+ obj = attemptResult.obj;
233
+ pySrc = attemptResult.pySrc;
234
+ genSecs = attemptResult.genSecs;
235
+ } else {
236
+ rdr = new PycReader(buffer);
237
+ try {
238
+ obj = rdr.ReadObject();
239
+ } catch (ex) {
240
+ if (ex instanceof PycReader.LoadError) {
241
+ // Save the binary file if it not already exists for future manual analysis.
242
+ if (!ex.FileName) {
243
+ if (global.g_cliArgs?.debug) {
244
+ console.log(`LoadError: ${ex.message} at position ${ex.position}`);
245
+ }
246
+ return;
247
+ }
248
+ filename = g_baseDir + ex.FileName;
249
+ let dirPath = Path.dirname(filename);
250
+ if (!g_cliArgs.skipPath && !fs.existsSync(dirPath)) {
251
+ fs.mkdirSync(dirPath, {recursive: true});
252
+ }
253
+ let filenamePyc = filename.substring(0, filename.lastIndexOf('.')) + ".pyc";
254
+ fs.writeFileSync(filenamePyc, rdr.Reader);
255
+ console.log(`Error: ${ex.message}\nFile: ${filenamePyc}\nPosition: ${ex.position}`);
256
+ return;
257
+ }
258
+ console.log(`Error: ${ex.message}\nStack:\n${ex.stacktrace}`);
113
259
  return;
114
260
  }
115
- console.log(`Error: ${ex.message}\nStack:\n${ex.stacktrace}`);
261
+ }
262
+
263
+ if (!obj) {
116
264
  return;
117
265
  }
266
+ filename = g_baseDir + obj.FileName;
118
267
 
119
268
  if (!g_cliArgs.sendToStdout) {
120
269
  console.log(`Processing ${filename}...`);
@@ -139,15 +288,18 @@ function decompilePycObject(data) {
139
288
  fs.writeFileSync(filenameBase + ".pyasm", PycDisassembler.Disassemble(rdr, obj));
140
289
  }
141
290
  }
142
- let genStartTS = process.hrtime.bigint();
143
- let decompiler = new PycDecompiler(obj);
144
- let ast = decompiler.decompile();
145
- let pycResult = ast.codeFragment();
146
- let pySrc = pycResult.toString();
291
+ if (!pySrc) {
292
+ let genStartTS = process.hrtime.bigint();
293
+ let decompiler = new PycDecompiler(obj);
294
+ let ast = decompiler.decompile();
295
+ let pycResult = ast.codeFragment();
296
+ pySrc = pycResult.toString();
297
+ genSecs = Number(process.hrtime.bigint() - genStartTS) / 1000000000;
298
+ }
147
299
  if (!pySrc.endsWith("\n")) {
148
300
  pySrc += "\n";
149
301
  }
150
- let genSecs = parseInt(process.hrtime.bigint() - genStartTS) / 1000000000;
302
+ genSecs = Math.max(genSecs, 0.000000001);
151
303
  if (g_cliArgs.sendToStdout) {
152
304
  // console.log(`\n\n${filenameBase}.${g_cliArgs.fileExt}\n-------\n${pySrc}`);
153
305
  console.log(pySrc);
@@ -156,9 +308,9 @@ function decompilePycObject(data) {
156
308
  }
157
309
  let secs = parseInt(process.hrtime.bigint() - startTS) / 1000000000;
158
310
  g_totalExecTime += secs;
159
- let inThroughput = data.length / genSecs;
311
+ let inThroughput = buffer.length / genSecs;
160
312
  let outThroughput = pySrc.length / genSecs;
161
- g_totalInThroughput += data.length;
313
+ g_totalInThroughput += buffer.length;
162
314
  g_totalOutThroughput += pySrc.length;
163
315
  g_totalFiles++;
164
316
  if (g_cliArgs.stats) {
@@ -196,6 +348,17 @@ function DecompileModule(filenames)
196
348
  }
197
349
 
198
350
  parseCLIParams()
351
+ if (g_cliArgs.pyVersion && !g_cliArgs.marshal) {
352
+ console.log("Error: --py-version requires --marshal (headerless input).");
353
+ process.exit(1);
354
+ }
355
+ if (g_cliArgs.pyVersion) {
356
+ g_pyVersionInfo = PycReader.ResolveVersionTag(g_cliArgs.pyVersion);
357
+ if (!g_pyVersionInfo) {
358
+ console.log(`Error: unsupported --py-version "${g_cliArgs.pyVersion}". Use format X.Y (e.g., 3.11).`);
359
+ process.exit(1);
360
+ }
361
+ }
199
362
  if (g_cliArgs.filenames.length === 0) {
200
363
  printUsage();
201
364
  process.exit(1);
@@ -712,7 +712,9 @@ class PycDecompiler {
712
712
  {
713
713
  PycDecompiler.opCodeHandlers[this.code.Current.OpCodeID].call(this);
714
714
  } else {
715
- console.error(`Unsupported opcode ${this.code.Current.InstructionName} at pos ${this.code.Current.Offset}\n`);
715
+ if (!g_cliArgs?.silent) {
716
+ console.error(`Unsupported opcode ${this.code.Current.InstructionName} at pos ${this.code.Current.Offset}\n`);
717
+ }
716
718
  this.cleanBuild = false;
717
719
  let node = new AST.ASTNodeList(this.defBlock.nodes);
718
720
  return node;
@@ -725,9 +727,11 @@ class PycDecompiler {
725
727
  && (this.curBlock.end == this.code.Next?.Offset);
726
728
 
727
729
  } catch (ex) {
728
- console.error(`EXCEPTION for OpCode ${this.code.Current.InstructionName} (${this.code.Current.Argument}) at offset ${this.code.Current.Offset} in code object '${this.object.Name}', file offset ${this.object.codeOffset + this.code.Current.Offset} : ${ex.message}\n\n`);
729
- if (global.g_cliArgs?.debug) {
730
- console.error('Stack trace:', ex.stack);
730
+ if (!g_cliArgs?.silent) {
731
+ console.error(`EXCEPTION for OpCode ${this.code.Current.InstructionName} (${this.code.Current.Argument}) at offset ${this.code.Current.Offset} in code object '${this.object.Name}', file offset ${this.object.codeOffset + this.code.Current.Offset} : ${ex.message}\n\n`);
732
+ if (global.g_cliArgs?.debug) {
733
+ console.error('Stack trace:', ex.stack);
734
+ }
731
735
  }
732
736
  }
733
737
  }
package/lib/PycReader.js CHANGED
@@ -103,6 +103,37 @@ const MagicToVersion = {
103
103
  0x0A0D0E2B: {major: 3, minor: 14, IsUnicode: true, opcode: require('./bytecode/python_3_14')}
104
104
  };
105
105
 
106
+ const VersionAliases = {
107
+ "1.2": "1.1"
108
+ };
109
+
110
+ const VersionToInfo = {};
111
+ for (const [magic, info] of Object.entries(MagicToVersion)) {
112
+ const key = `${info.major}.${info.minor}`;
113
+ const existing = VersionToInfo[key];
114
+ const candidate = {...info, magic: Number(magic)};
115
+ if (!existing || (candidate.revision || 0) > (existing.revision || 0)) {
116
+ VersionToInfo[key] = candidate;
117
+ }
118
+ }
119
+
120
+ function parseVersionTag(tag) {
121
+ if (!tag) {
122
+ return null;
123
+ }
124
+ const cleaned = String(tag).trim().replace(/^python/i, '').replace(/^py/i, '');
125
+ const match = cleaned.match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?$/);
126
+ if (!match || match[2] === undefined) {
127
+ return null;
128
+ }
129
+ const major = parseInt(match[1], 10);
130
+ const minor = parseInt(match[2], 10);
131
+ if (Number.isNaN(major) || Number.isNaN(minor)) {
132
+ return null;
133
+ }
134
+ return {major, minor};
135
+ }
136
+
106
137
 
107
138
  class PycReader
108
139
  {
@@ -123,11 +154,14 @@ class PycReader
123
154
  m_filename = null;
124
155
  m_version = null;
125
156
 
126
- constructor(data) {
157
+ constructor(data, options = {}) {
127
158
  if (!data) {
128
159
  return;
129
160
  }
130
161
 
162
+ const opts = options || {};
163
+ this.m_filename = opts.filename || null;
164
+
131
165
  let buffer = data;
132
166
  if (typeof(data) == 'string') {
133
167
  buffer = fs.readFileSync(data);
@@ -135,6 +169,21 @@ class PycReader
135
169
  throw new Error('PycReader accepts only String as a file path or Buffer as content.');
136
170
  }
137
171
  this.m_rdr = new BinaryReader(buffer);
172
+
173
+ if (opts.marshal) {
174
+ let versionInfo = opts.versionInfo;
175
+ if (!versionInfo && opts.pyVersion) {
176
+ versionInfo = PycReader.ResolveVersionTag(opts.pyVersion);
177
+ }
178
+ if (!versionInfo) {
179
+ throw new Error('Marshal mode requires a bytecode version. Pass --py-version or pre-resolve it.');
180
+ }
181
+ this.m_version = versionInfo;
182
+ if (global.g_cliArgs?.debug && !opts.silent) {
183
+ console.log(`Marshal mode: Python ${this.m_version.major}.${this.m_version.minor}`);
184
+ }
185
+ return;
186
+ }
138
187
 
139
188
  let marshalVersion = this.m_rdr.readUInt32();
140
189
  this.m_version = MagicToVersion[marshalVersion] || MagicToVersion[marshalVersion & 0xFFFFFFFE] || {major: -1, minor: -1, IsUnicode: false};
@@ -190,6 +239,129 @@ class PycReader
190
239
  return this.m_version.opcode;
191
240
  }
192
241
 
242
+ static ResolveVersionTag(tag) {
243
+ const parsed = parseVersionTag(tag);
244
+ if (!parsed) {
245
+ return null;
246
+ }
247
+ const key = `${parsed.major}.${parsed.minor}`;
248
+ const aliased = VersionAliases[key] ? VersionAliases[key] : key;
249
+ return VersionToInfo[aliased] || null;
250
+ }
251
+
252
+ static ListSupportedVersions(desc = true) {
253
+ const list = Object.values(VersionToInfo);
254
+ list.sort((a, b) => {
255
+ if (a.major !== b.major) {
256
+ return a.major - b.major;
257
+ }
258
+ return a.minor - b.minor;
259
+ });
260
+ return desc ? list.reverse() : list;
261
+ }
262
+
263
+ static GuessVersion(buffer) {
264
+ const candidates = PycReader.ListSupportedVersions(true);
265
+ let best = null;
266
+ for (const candidate of candidates) {
267
+ const trial = PycReader.TryParseMarshal(buffer, candidate);
268
+ if (!trial) {
269
+ continue;
270
+ }
271
+ if (trial.unknown === 0 && trial.remaining === 0) {
272
+ return candidate;
273
+ }
274
+ if (!best ||
275
+ trial.unknownRatio < best.unknownRatio ||
276
+ (trial.unknownRatio === best.unknownRatio && trial.remaining < best.remaining)) {
277
+ best = {...trial, candidate};
278
+ }
279
+ }
280
+ return best ? best.candidate : null;
281
+ }
282
+
283
+ static CountUnknownOpcodes(codeObject, reader, opCodeList) {
284
+ const code = codeObject?.Code?.Value;
285
+ if (!code || !opCodeList) {
286
+ return {unknown: Number.MAX_SAFE_INTEGER, total: 0};
287
+ }
288
+ const wordSize = reader.getInstructionWordSize();
289
+ let unknown = 0;
290
+ let total = 0;
291
+ if (wordSize === 2) {
292
+ for (let offset = 0; offset < code.length; offset += 2) {
293
+ total++;
294
+ if (!opCodeList[code[offset]]) {
295
+ unknown++;
296
+ }
297
+ }
298
+ } else {
299
+ let offset = 0;
300
+ while (offset < code.length) {
301
+ total++;
302
+ const opcode = code[offset];
303
+ const entry = opCodeList[opcode];
304
+ if (!entry) {
305
+ unknown++;
306
+ offset += 1;
307
+ continue;
308
+ }
309
+ offset += entry.HasArgument ? 3 : 1;
310
+ }
311
+ }
312
+ return {unknown, total};
313
+ }
314
+
315
+ static TryParseMarshal(buffer, versionInfo) {
316
+ try {
317
+ const reader = new PycReader(buffer, {marshal: true, versionInfo, silent: true});
318
+ const obj = reader.ReadObject();
319
+ if (!obj || obj.ClassName !== "Py_CodeObject") {
320
+ return null;
321
+ }
322
+ const remaining = reader.m_rdr.Reader.length - reader.m_rdr.pc;
323
+ if (remaining < 0) {
324
+ return null;
325
+ }
326
+
327
+ const prevDebug = global.g_cliArgs?.debug;
328
+ if (global.g_cliArgs) {
329
+ global.g_cliArgs.debug = false;
330
+ }
331
+ let opcodes = null;
332
+ try {
333
+ opcodes = new versionInfo.opcode(obj);
334
+ } finally {
335
+ if (global.g_cliArgs) {
336
+ global.g_cliArgs.debug = prevDebug;
337
+ }
338
+ }
339
+
340
+ const {unknown, total} = PycReader.CountUnknownOpcodes(obj, reader, opcodes?.OpCodeList);
341
+ const unknownRatio = total > 0 ? unknown / total : 1;
342
+
343
+ let score = 0;
344
+ if (remaining === 0) {
345
+ score += 3;
346
+ }
347
+ if (unknown === 0) {
348
+ score += 2;
349
+ }
350
+ if (obj.Code?.Value && Buffer.isBuffer(obj.Code.Value)) {
351
+ score += 1;
352
+ }
353
+ if (obj.Consts?.ClassName === "Py_Tuple") {
354
+ score += 1;
355
+ }
356
+ if (obj.Names?.ClassName === "Py_Tuple") {
357
+ score += 1;
358
+ }
359
+ return {score, remaining, unknown, unknownRatio};
360
+ } catch (ex) {
361
+ return null;
362
+ }
363
+ }
364
+
193
365
  ReadObject() {
194
366
  try {
195
367
  let obj = new PythonObject(), value = null;
@@ -1863,7 +1863,7 @@ class ASTKwNamesMap extends ASTNode {
1863
1863
 
1864
1864
  constructor(values) {
1865
1865
  super();
1866
- this.m_values = values;
1866
+ this.m_values = Array.isArray(values) ? values : [];
1867
1867
  }
1868
1868
 
1869
1869
  get values() {
@@ -1876,6 +1876,9 @@ class ASTKwNamesMap extends ASTNode {
1876
1876
  }
1877
1877
 
1878
1878
  add (key, value) {
1879
+ if (!this.m_values) {
1880
+ this.m_values = [];
1881
+ }
1879
1882
  this.values.push({key, value});
1880
1883
  }
1881
1884
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "depyo",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Python bytecode decompiler (Python 1.0–3.14) implemented in Node.js",
5
5
  "bin": {
6
6
  "depyo": "./depyo.js"