xasm 1.2.0__py36-none-any.whl → 1.2.1.dev0__py36-none-any.whl

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.
xasm/assemble.py CHANGED
@@ -1,27 +1,35 @@
1
1
  #!/usr/bin/env python
2
- import ast, re, xdis
3
- from xdis.opcodes.base import cmp_op
2
+ import ast
3
+ import re
4
+ from typing import Optional
5
+
6
+ import xdis
4
7
  from xdis import get_opcode, load_module
8
+ from xdis.opcodes.base import cmp_op
5
9
  from xdis.version_info import PYTHON_VERSION_TRIPLE, version_str_to_tuple
6
10
 
7
11
  # import xdis.bytecode as Mbytecode
8
12
 
9
- class Instruction(object): # (Mbytecode.Instruction):
10
- def __repr__(self):
11
- s = ""
13
+
14
+ class Instruction: # (Mbytecode.Instruction):
15
+ line_no: Optional[int]
16
+ opname: str
17
+ arg: Optional[int]
18
+
19
+ def __repr__(self) -> str:
12
20
  if self.line_no:
13
21
  s = "%4d: " % self.line_no
14
22
  else:
15
23
  s = " " * 6
16
24
  s += "%-15s" % self.opname
17
25
  if self.arg is not None:
18
- s += "\t%s" % self.arg
26
+ s += f"\t{self.arg}"
19
27
  return s
20
28
 
21
29
  pass
22
30
 
23
31
 
24
- def is_int(s):
32
+ def is_int(s) -> bool:
25
33
  try:
26
34
  int(s)
27
35
  return True
@@ -29,7 +37,7 @@ def is_int(s):
29
37
  return False
30
38
 
31
39
 
32
- def is_lineno(s):
40
+ def match_lineno(s: str):
33
41
  return re.match(r"^\d+:", s)
34
42
 
35
43
 
@@ -42,9 +50,9 @@ def get_opname_operand(opc, fields):
42
50
  if is_int(fields[1]):
43
51
  operand = int(fields[1])
44
52
  else:
45
- operand = ' '.join(fields[1:])
53
+ operand = " ".join(fields[1:])
46
54
  if operand.startswith("(to "):
47
- int_val = operand[len("(to "):]
55
+ int_val = operand[len("(to ") :]
48
56
  # In xasm format this shouldn't appear
49
57
  if is_int(int_val):
50
58
  operand = int(int_val)
@@ -54,12 +62,12 @@ def get_opname_operand(opc, fields):
54
62
  return opname, None
55
63
 
56
64
 
57
- class Assembler(object):
65
+ class Assembler:
58
66
  def __init__(self, python_version, is_pypy):
59
67
  self.opc = get_opcode(python_version, is_pypy)
60
68
  self.code_list = []
61
69
  self.codes = [] # FIXME use a better name
62
- self.status = "unfinished"
70
+ self.status: str = "unfinished"
63
71
  self.size = 0 # Size of source code. Only relevant in version 3 and above
64
72
  self.python_version = python_version
65
73
  self.timestamp = None
@@ -69,7 +77,6 @@ class Assembler(object):
69
77
  self.siphash = None
70
78
 
71
79
  def code_init(self, python_version=None):
72
-
73
80
  if self.python_version is None and python_version:
74
81
  self.python_version = python_version
75
82
 
@@ -107,8 +114,19 @@ class Assembler(object):
107
114
  print()
108
115
  print(inst)
109
116
 
110
- def err(self, mess):
111
- print(mess)
117
+ def warn(self, mess: str):
118
+ """
119
+ Print an error message and record that we warned, unless we have already errored.
120
+ """
121
+ print("Warning: ", mess)
122
+ if self.status != "errored":
123
+ self.status = "warning"
124
+
125
+ def err(self, mess: str):
126
+ """
127
+ Print an error message and record that we errored.
128
+ """
129
+ print("Error: ", mess)
112
130
  self.status = "errored"
113
131
 
114
132
 
@@ -119,26 +137,35 @@ def asm_file(path):
119
137
  asm = None
120
138
  backpatch_inst = set([])
121
139
  label = {}
122
- python_version = None
140
+ python_bytecode_version = None
123
141
  lines = open(path).readlines()
124
142
  i = 0
125
143
  bytecode_seen = False
126
144
  while i < len(lines):
127
145
  line = lines[i]
128
146
  i += 1
147
+ if line.startswith("##"):
148
+ # comment line
149
+ continue
129
150
  if line.startswith(".READ"):
130
151
  match = re.match("^.READ (.+)$", line)
131
152
  if match:
132
153
  input_pyc = match.group(1)
133
154
  print(f"Reading {input_pyc}")
134
- (version, timestamp, magic_int, co, is_pypy, source_size, sip_hash) = load_module(
135
- input_pyc
136
- )
137
- if python_version and python_version != version:
155
+ (
156
+ version,
157
+ timestamp,
158
+ magic_int,
159
+ co,
160
+ is_pypy,
161
+ source_size,
162
+ sip_hash,
163
+ ) = load_module(input_pyc)
164
+ if python_bytecode_version and python_bytecode_version != version:
138
165
  TypeError(
139
- f"We previously saw Python version {python_version} but we just loaded {version}.\n"
166
+ f"We previously saw Python version {python_bytecode_version} but we just loaded {version}.\n"
140
167
  )
141
- python_version = version
168
+ python_bytecode_version = version
142
169
  # FIXME: extract all code options below the top-level and asm.code_list
143
170
 
144
171
  elif line.startswith("#"):
@@ -151,15 +178,18 @@ def asm_file(path):
151
178
  is_pypy = False
152
179
  pypy_str = ""
153
180
 
154
- version = (
181
+ python_bytecode_version = (
155
182
  line[len("# Python bytecode " + pypy_str) :].strip().split()[0]
156
183
  )
157
184
 
158
- python_version_pair = version_str_to_tuple(python_version, len=2)
185
+ python_version_pair = version_str_to_tuple(
186
+ python_bytecode_version, length=2
187
+ )
159
188
  asm = Assembler(python_version_pair, is_pypy)
160
189
  if python_version_pair >= (3, 10):
161
190
  TypeError(
162
- f"Creating Python version {python_version} not supported yet. Feel free to fix and put in a PR.\n"
191
+ f"Creating Python version {python_bytecode_version} not supported yet. "
192
+ "Feel free to fix and put in a PR.\n"
163
193
  )
164
194
  asm.code_init(python_version_pair)
165
195
  bytecode_seen = True
@@ -170,15 +200,25 @@ def asm_file(path):
170
200
  asm.timestamp = int(time_str)
171
201
  elif line.startswith("# Method Name: "):
172
202
  if method_name:
173
- co = create_code(asm, label, backpatch_inst)
203
+ co, is_valid = create_code(asm, label, backpatch_inst)
204
+ if not is_valid:
205
+ return
174
206
  asm.update_lists(co, label, backpatch_inst)
175
207
  label = {}
176
208
  backpatch_inst = set([])
177
209
  methods[method_name] = co
178
210
  offset = 0
179
- python_version_pair = version_str_to_tuple(python_version, len=2)
211
+ if python_bytecode_version is None:
212
+ raise TypeError(
213
+ f'Line {i}: "Python bytecode" not seen before "Method Name:"; please set this.'
214
+ )
215
+ python_version_pair = version_str_to_tuple(
216
+ python_bytecode_version, length=2
217
+ )
180
218
  asm.code_init(python_version_pair)
181
- asm.code.co_name = line[len("# Method Name: ") :].strip()
219
+ asm.code.co_qual_name = asm.code.co_name = line[
220
+ len("# Method Name: ") :
221
+ ].strip()
182
222
  method_name = asm.code.co_name
183
223
  elif line.startswith("# SipHash: "):
184
224
  siphash = line[len("# ShipHash: ") :].strip().split()[0]
@@ -227,9 +267,8 @@ def asm_file(path):
227
267
  if match:
228
268
  index = int(match.group(1))
229
269
  assert index == count, (
230
- "Constant index {%d} found on line {%d}"
231
- "doesn't match expected constant index {%d}."
232
- % (index, i, count)
270
+ f"Constant index {index} found on line {i} "
271
+ f"doesn't match expected constant index {count}."
233
272
  )
234
273
  expr = match.group(2)
235
274
  match = re.match(
@@ -239,7 +278,7 @@ def asm_file(path):
239
278
  name = match.group(1)
240
279
  m2 = re.match("^<(.+)>$", name)
241
280
  if m2:
242
- name = "%s_%s" % (m2.group(1), match.group(2))
281
+ name = f"{m2.group(1)}_{match.group(2)}"
243
282
  if name in methods:
244
283
  asm.code.co_consts.append(methods[name])
245
284
  else:
@@ -248,7 +287,7 @@ def asm_file(path):
248
287
  )
249
288
  bogus_name = f"**bogus {name}**"
250
289
  print(f"\t appending {bogus_name} to list of constants")
251
- asm.code.co_consts.append()
290
+ asm.code.co_consts.append(bogus_name)
252
291
  else:
253
292
  asm.code.co_consts.append(ast.literal_eval(expr))
254
293
  count += 1
@@ -276,46 +315,73 @@ def asm_file(path):
276
315
  if not line.strip():
277
316
  continue
278
317
 
279
- match = re.match(r"^([^\s]+):$", line)
318
+ match = re.match(r"^(\S+):$", line)
280
319
  if match:
281
- label[match.group(1)] = offset
282
- continue
320
+ label_value = match.group(1)
321
+ # All-numeric labels, i.e. line numbers, are handled below
322
+ if not re.match(r"^(\d+)$", label_value):
323
+ label[label_value] = offset
324
+ continue
325
+
326
+ line_no = None
327
+
328
+ match = re.match(r"^\s*(\d+):\s*", line)
329
+
330
+ # Sanity checking: make sure we have seen
331
+ # proper header lines
332
+ if i == 1:
333
+ assert bytecode_seen, (
334
+ f"Improper beginning:\n{line}"
335
+ "\nLine should begin with '#' "
336
+ "and contain header bytecode header information."
337
+ )
338
+ assert bytecode_seen, (
339
+ f"Error translating line {i}: "
340
+ "a line before this should include: \n"
341
+ "# Python bytecode <version>"
342
+ )
283
343
 
284
- match = re.match(r"^\s*([\d]+):\s*$", line)
285
344
  if match:
286
345
  line_no = int(match.group(1))
287
- asm.code.co_lnotab[offset] = line_no
288
- continue
346
+ linetable_field = (
347
+ "co_lnotab" if python_version_pair < (3, 10) else "co_linetable"
348
+ )
349
+ assert asm is not None
350
+ linetable = getattr(asm.code, linetable_field)
351
+ linetable[offset] = line_no
289
352
 
290
353
  # Opcode section
291
- assert (
292
- bytecode_seen
293
- ), "File needs to start out with: # Python bytecode <version>"
294
354
  fields = line.strip().split()
295
- line_no = None
296
355
  num_fields = len(fields)
297
356
 
298
- if num_fields > 1:
299
- if fields[0] == ">>":
300
- fields = fields[1:]
301
- num_fields -= 1
302
- if is_lineno(fields[0]) and is_int(fields[1]):
303
- line_no = int(fields[0][:-1])
304
- opname, operand = get_opname_operand(asm.opc, fields[2:])
305
- elif is_lineno(fields[0]):
306
- line_no = int(fields[0][:-1])
307
- fields = fields[1:]
357
+ if num_fields == 1 and line_no is not None:
358
+ continue
359
+
360
+ try:
361
+ if num_fields > 1:
308
362
  if fields[0] == ">>":
309
363
  fields = fields[1:]
310
- if is_int(fields[0]):
364
+ num_fields -= 1
365
+ if match_lineno(fields[0]) and is_int(fields[1]):
366
+ line_no = int(fields[0][:-1])
367
+ opname, operand = get_opname_operand(asm.opc, fields[2:])
368
+ elif match_lineno(fields[0]):
369
+ line_no = int(fields[0][:-1])
370
+ fields = fields[1:]
371
+ if fields[0] == ">>":
311
372
  fields = fields[1:]
312
- opname, operand = get_opname_operand(asm.opc, fields)
313
- elif is_int(fields[0]):
314
- opname, operand = get_opname_operand(asm.opc, fields[1:])
373
+ if is_int(fields[0]):
374
+ fields = fields[1:]
375
+ opname, operand = get_opname_operand(asm.opc, fields)
376
+ elif is_int(fields[0]):
377
+ opname, operand = get_opname_operand(asm.opc, fields[1:])
378
+ else:
379
+ opname, operand = get_opname_operand(asm.opc, fields)
315
380
  else:
316
- opname, operand = get_opname_operand(asm.opc, fields)
317
- else:
318
- opname, _ = get_opname_operand(asm.opc, fields)
381
+ opname, _ = get_opname_operand(asm.opc, fields)
382
+ except Exception as e:
383
+ print(f"Line {i}: {e}")
384
+ raise
319
385
 
320
386
  if opname in asm.opc.opname:
321
387
  inst = Instruction()
@@ -332,15 +398,18 @@ def asm_file(path):
332
398
  backpatch_inst.add(inst)
333
399
  offset += xdis.op_size(inst.opcode, asm.opc)
334
400
  else:
335
- raise RuntimeError("Illegal opname %s in:\n%s" % (opname, line))
401
+ raise RuntimeError(f"Illegal opname {opname} in:\n{line}")
336
402
  pass
337
403
  pass
338
- # print(asm.code.co_lnotab)
339
- if asm:
340
- co = create_code(asm, label, backpatch_inst)
404
+
405
+ if asm is not None:
406
+ # print(linetable)
407
+
408
+ co, is_valid = create_code(asm, label, backpatch_inst)
341
409
  asm.update_lists(co, label, backpatch_inst)
342
- asm.code_list.reverse()
343
- asm.status = "finished"
410
+ asm.code_list.reverse()
411
+ asm.status = "finished"
412
+
344
413
  return asm
345
414
 
346
415
 
@@ -372,7 +441,9 @@ def update_code_tuple_field(field_name, code, lines, i):
372
441
  match = re.match(r"^#\s+(\d+): (.+)$", line)
373
442
  if match:
374
443
  index = int(match.group(1))
375
- assert index == count
444
+ assert (
445
+ index == count
446
+ ), f'In field" "{field_name}", line {i}, number {index} is expected to have value {count}.'
376
447
  field_values = getattr(code, field_name)
377
448
  field_values.append(match.group(2))
378
449
  count += 1
@@ -380,7 +451,6 @@ def update_code_tuple_field(field_name, code, lines, i):
380
451
  i -= 1
381
452
  break
382
453
  pass
383
- pass
384
454
  return i
385
455
 
386
456
 
@@ -389,8 +459,18 @@ def err(msg, inst, i):
389
459
  raise RuntimeError(msg)
390
460
 
391
461
 
392
- def decode_lineno_tab(lnotab, first_lineno):
462
+ def warn(mess: str):
463
+ """
464
+ Print an error message and record that we warned.
465
+ """
466
+ print("Warning: ", mess)
467
+
393
468
 
469
+ def decode_lineno_tab_old(lnotab, first_lineno: int) -> dict:
470
+ """
471
+ Uncompresses line number table for Python versions before
472
+ 3.10
473
+ """
394
474
  line_number, line_number_diff = first_lineno, 0
395
475
  offset, offset_diff = 0, 0
396
476
  uncompressed_lnotab = {}
@@ -409,28 +489,138 @@ def decode_lineno_tab(lnotab, first_lineno):
409
489
  continue
410
490
  line_number += line_number_diff
411
491
  offset += offset_diff
412
- line_number_diff, offset_diff = 0, 0
413
492
  uncompressed_lnotab[offset] = line_number
414
493
 
415
494
  return uncompressed_lnotab
416
495
 
417
496
 
418
- def create_code(asm, label, backpatch):
497
+ def is_code_ok(asm: Assembler) -> bool:
498
+ """
499
+ Performs some sanity checks on code.
500
+ """
501
+
502
+ is_valid: bool = True
503
+
504
+ code = asm.code
505
+ last_instruction = code.instructions[-1]
506
+ last_offset = last_instruction.offset
507
+ if last_instruction.opname not in ("RETURN_VALUE", "RERAISE", "RAISE_VARARGS"):
508
+ warn(
509
+ f"Last instruction of at offset {last_offset} of {code.co_name}"
510
+ f' should be "RETURN_VALUE", is "{last_instruction.opname}"'
511
+ )
512
+ is_valid = False
513
+
514
+ cells_free_len = len(code.co_freevars) + len(code.co_cellvars)
515
+ consts_len = len(code.co_consts)
516
+ names_len = len(code.co_names)
517
+ varnames_len = len(code.co_varnames)
518
+
519
+ for i, inst in enumerate(code.instructions):
520
+ if xdis.op_has_argument(inst.opcode, asm.opc):
521
+ if is_int(inst.arg):
522
+ if inst.opcode == asm.opc.EXTENDED_ARG:
523
+ continue
524
+ operand = inst.arg
525
+ if inst.opcode in asm.opc.CONST_OPS:
526
+ # FIXME: DRY operand check
527
+ if operand >= consts_len:
528
+ print(inst)
529
+ warn(
530
+ f"Constant operand index {operand} at offset {inst.offset} of {code.co_name} "
531
+ f"is too large; it should be less than {consts_len}."
532
+ )
533
+ is_valid = False
534
+ elif inst.opcode in asm.opc.LOCAL_OPS:
535
+ if operand >= varnames_len:
536
+ print(inst)
537
+ warn(
538
+ f"Variable operand index {operand} at offset {inst.offset} of {code.co_name} "
539
+ f"is too large; it should be less than {varnames_len}."
540
+ )
541
+ is_valid = False
542
+ elif inst.opcode in asm.opc.NAME_OPS:
543
+ if operand >= names_len:
544
+ print(inst)
545
+ warn(
546
+ f"Name operand index {operand} at offset {inst.offset} of {code.co_name} "
547
+ f"is too large; it should be less than {names_len}."
548
+ )
549
+ is_valid = False
550
+ elif inst.opcode in asm.opc.FREE_OPS:
551
+ # FIXME: is this right?
552
+ if operand >= cells_free_len:
553
+ print(inst)
554
+ warn(
555
+ f"Free operand index {operand} at offset {inst.offset} of {code.co_name} "
556
+ f"is too large; it should be less than {cells_free_len}."
557
+ )
558
+ is_valid = False
559
+
560
+ return is_valid
561
+
562
+
563
+ def append_operand(
564
+ bytecode: list, arg_value, extended_arg_shift, arg_max_value, extended_arg_op
565
+ ):
566
+ """
567
+ Write instruction operand adding EXTENDED_ARG instructions
568
+ when necessary.
569
+ """
570
+ arg_shifts = []
571
+ shift_value = 1
572
+
573
+ while arg_value > arg_max_value:
574
+ shift_value <<= extended_arg_shift
575
+ ext_arg_value, arg_value = divmod(arg_value, shift_value)
576
+ arg_shifts.append(ext_arg_value)
577
+
578
+ while arg_shifts:
579
+ bytecode.append(extended_arg_op)
580
+ ext_arg_value = arg_shifts.pop()
581
+ bytecode.append(ext_arg_value)
582
+
583
+ bytecode.append(arg_value)
584
+
585
+
586
+ def create_code(asm: Assembler, label, backpatch) -> tuple:
587
+ """
588
+ Turn ``asm`` assembler text into a code object and
589
+ return that.
590
+ """
419
591
  # print('label: ', asm.label)
420
592
  # print('backpatch: ', asm.backpatch_inst)
421
593
 
422
- bcode = []
594
+ bytecode = []
423
595
  # print(asm.code.instructions)
424
596
 
425
597
  offset = 0
426
- extended_value = 0
427
598
  offset2label = {label[j]: j for j in label}
599
+ is_valid = True
428
600
 
429
601
  for i, inst in enumerate(asm.code.instructions):
430
- bcode.append(inst.opcode)
602
+ # Strip out extended arg instructions.
603
+ # Operands in the input can be arbitary numbers.
604
+ # In this loop we will figure out whether
605
+ # or not to add EXTENDED_ARG
606
+ if inst.opcode == asm.opc.EXTENDED_ARG:
607
+ print(
608
+ f"Line {i}: superflous EXTENDED_ARG instruction removed;"
609
+ " this code decides when they are needed."
610
+ )
611
+ continue
612
+
613
+ bytecode.append(inst.opcode)
431
614
  if offset in offset2label:
432
615
  if is_int(offset2label[offset]):
433
616
  inst.line_no = int(offset2label[offset])
617
+ if (
618
+ inst.line_no in asm.code.co_lnotab.values()
619
+ and asm.python_version < (3, 10)
620
+ ):
621
+ print(
622
+ f"Line {i}: this is not the first we encounter source-code line {inst.line_no}."
623
+ )
434
624
  asm.code.co_lnotab[offset] = inst.line_no
435
625
 
436
626
  inst.offset = offset
@@ -447,17 +637,13 @@ def create_code(asm, label, backpatch):
447
637
  inst.arg = label[target] - offset
448
638
  else:
449
639
  inst.arg = label[target]
450
- pass
640
+ if asm.opc.version_tuple >= (3, 10):
641
+ inst.arg >>= 1
451
642
  pass
452
643
  except KeyError:
453
644
  err(f"Label {target} not found.\nI know about {backpatch}", inst, i)
645
+ is_valid = False
454
646
  elif is_int(inst.arg):
455
- if inst.opcode == asm.opc.EXTENDED_ARG:
456
- extended_value += inst.arg
457
- if asm.opc.version >= 3.6:
458
- extended_value <<= 8
459
- else:
460
- extended_value <<= 16
461
647
  pass
462
648
  elif inst.arg.startswith("(") and inst.arg.endswith(")"):
463
649
  operand = inst.arg[1:-1]
@@ -465,11 +651,13 @@ def create_code(asm, label, backpatch):
465
651
  if operand in cmp_op:
466
652
  inst.arg = cmp_op.index(operand)
467
653
  else:
468
- err("Can't handle compare operand %s" % inst.arg, inst, i)
654
+ err(f"Can't handle compare operand {inst.arg}", inst, i)
655
+ is_valid = False
656
+ break
469
657
 
470
658
  pass
471
659
  elif inst.opcode in asm.opc.CONST_OPS:
472
- if not operand.startswith("<Code"):
660
+ if not (operand.startswith("<Code") or operand.startswith("<code")):
473
661
  operand = ast.literal_eval(operand)
474
662
  update_code_field("co_consts", operand, inst, asm.code)
475
663
  elif inst.opcode in asm.opc.LOCAL_OPS:
@@ -483,47 +671,52 @@ def create_code(asm, label, backpatch):
483
671
  update_code_field("co_freevars", operand, inst, asm.code)
484
672
  else:
485
673
  # from trepan.api import debug; debug()
486
- err("Can't handle operand %s" % inst.arg, inst, i)
674
+ err(f"Can't handle operand {inst.arg}", inst, i)
675
+ is_valid = False
676
+ break
487
677
  else:
488
678
  # from trepan.api import debug; debug()
489
679
  err(
490
- "Don't understand operand %s expecting int or (..)" % inst.arg,
680
+ f"Don't understand operand {inst.arg} expecting int or (..)",
491
681
  inst,
492
682
  i,
493
683
  )
494
684
 
495
- if asm.opc.version_tuple < (3, 6):
496
- if inst.opcode == asm.opc.EXTENDED_ARG:
497
- arg_tup = xdis.util.num2code(inst.arg)
498
- else:
499
- arg_tup = xdis.util.num2code(inst.arg - extended_value)
500
- extended_value = 0
501
- bcode += arg_tup
502
- # 3.6
503
- else:
504
- if inst.opcode == asm.opc.EXTENDED_ARG:
505
- bcode.append(inst.arg)
506
- else:
507
- bcode.append(inst.arg - extended_value)
508
- extended_value = 0
685
+ append_operand(
686
+ bytecode,
687
+ inst.arg,
688
+ asm.opc.EXTENDED_ARG_SHIFT,
689
+ asm.opc.ARG_MAX_VALUE,
690
+ asm.opc.EXTENDED_ARG,
691
+ )
692
+
509
693
  elif asm.opc.version_tuple >= (3, 6):
510
- bcode.append(0)
694
+ # instructions with no operand, or one-byte instructions, are padded
695
+ # to two bytes in 3.6 and later.
696
+ bytecode.append(0)
697
+
698
+ if not is_valid:
699
+ return None, False
511
700
 
512
701
  if asm.opc.version_tuple >= (3, 0):
513
702
  co_code = bytearray()
514
- for j in bcode:
703
+ for j in bytecode:
515
704
  co_code.append(j % 255)
516
705
  asm.code.co_code = bytes(co_code)
517
706
  else:
518
- asm.code.co_code = "".join([chr(j) for j in bcode])
707
+ asm.code.co_code = "".join([chr(j) for j in bytecode])
708
+
709
+ # FIXME: get
710
+ is_code_ok(asm)
519
711
 
520
712
  # Stamp might be added here
521
713
  if asm.python_version[:2] == PYTHON_VERSION_TRIPLE[:2]:
522
714
  code = asm.code.to_native()
523
715
  else:
524
716
  code = asm.code.freeze()
717
+
525
718
  # asm.print_instructions()
526
719
 
527
720
  # print (*args)
528
721
  # co = self.Code(*args)
529
- return code
722
+ return code, is_valid
xasm/pyc_convert.py CHANGED
@@ -2,25 +2,21 @@
2
2
  """Convert Python Bytecode from one version to another for
3
3
  some limited set of Python bytecode versions
4
4
  """
5
- from xdis import disassemble_file, load_module, magic2int, write_bytecode_file
6
- from xasm.write_pyc import write_pycfile
7
- import xdis
8
- from xdis.opcodes import opcode_33, opcode_27
9
- from tempfile import NamedTemporaryFile
10
- import os.path as osp
11
5
  import os
6
+ import os.path as osp
12
7
  from copy import copy
13
- from xasm.assemble import (
14
- asm_file,
15
- Assembler,
16
- create_code,
17
- Instruction,
18
- decode_lineno_tab,
19
- )
20
- from xdis.magics import magics
8
+ from tempfile import NamedTemporaryFile
9
+
21
10
  import click
11
+ import xdis
12
+ from xdis import disassemble_file, load_module, magic2int, write_bytecode_file
13
+ from xdis.magics import magics
14
+ from xdis.opcodes import opcode_27, opcode_33
22
15
 
16
+ from xasm.assemble import (Assembler, Instruction, asm_file, create_code,
17
+ decode_lineno_tab_old)
23
18
  from xasm.version import __version__
19
+ from xasm.write_pyc import write_pycfile
24
20
 
25
21
 
26
22
  def add_credit(asm, src_version, dest_version):
@@ -40,10 +36,10 @@ def copy_magic_into_pyc(input_pyc, output_pyc, src_version, dest_version):
40
36
  (version, timestamp, magic_int, co, is_pypy, source_size) = load_module(input_pyc)
41
37
  assert version == float(
42
38
  src_version
43
- ), "Need Python %s bytecode; got bytecode for version %s" % (src_version, version)
39
+ ), f"Need Python {src_version} bytecode; got bytecode for version {version}"
44
40
  magic_int = magic2int(magics.magics[dest_version])
45
41
  write_bytecode_file(output_pyc, co, magic_int)
46
- print("Wrote %s" % output_pyc)
42
+ print(f"Wrote {output_pyc}")
47
43
  return
48
44
 
49
45
 
@@ -146,8 +142,7 @@ def transform_33_32(inst, new_inst, i, n, offset, instructions, new_asm):
146
142
 
147
143
 
148
144
  def transform_asm(asm, conversion_type, src_version, dest_version):
149
-
150
- new_asm = Assembler(dest_version)
145
+ new_asm = Assembler(dest_version, is_pypy=False)
151
146
  for field in "code size".split():
152
147
  setattr(new_asm, field, copy(getattr(asm, field)))
153
148
 
@@ -158,13 +153,13 @@ def transform_asm(asm, conversion_type, src_version, dest_version):
158
153
  elif conversion_type == "33-32":
159
154
  transform_fn = transform_33_32
160
155
  else:
161
- raise RuntimeError("Don't know how to covert %s " % conversion_type)
156
+ raise RuntimeError(f"Don't know how to convert {conversion_type} ")
162
157
  for j, code in enumerate(asm.code_list):
163
158
  offset2label = {v: k for k, v in asm.label[j].items()}
164
159
  new_asm.backpatch.append(copy(asm.backpatch[j]))
165
160
  new_asm.label.append(copy(asm.label[j]))
166
161
  new_asm.codes.append(copy(code))
167
- new_asm.code.co_lnotab = decode_lineno_tab(code.co_lnotab, code.co_firstlineno)
162
+ new_asm.code.co_lnotab = decode_lineno_tab_old(code.co_lnotab, code.co_firstlineno)
168
163
  instructions = asm.codes[j].instructions
169
164
  new_asm.code.instructions = []
170
165
  i, offset, n = 0, 0, len(instructions)
@@ -182,14 +177,14 @@ def transform_asm(asm, conversion_type, src_version, dest_version):
182
177
  i += 1
183
178
  pass
184
179
 
185
- co = create_code(new_asm, new_asm.label[-1], new_asm.backpatch[-1])
180
+ co, is_valid = create_code(new_asm, new_asm.label[-1], new_asm.backpatch[-1])
186
181
  new_asm.code_list.append(co)
187
182
  new_asm.code_list.reverse()
188
- new_asm.finished = "finished"
183
+ new_asm.status = "finished" if is_valid else "invalid"
189
184
  return new_asm
190
185
 
191
186
 
192
- UPWARD_COMPATABLE = tuple("20-21 21-22 23-24 24-23".split())
187
+ UPWARD_COMPATIBLE = tuple("20-21 21-22 23-24 24-23".split())
193
188
 
194
189
 
195
190
  @click.command()
@@ -212,15 +207,15 @@ UPWARD_COMPATABLE = tuple("20-21 21-22 23-24 24-23".split())
212
207
  help="specify conversion from/to bytecode",
213
208
  default="26-27",
214
209
  )
215
- @click.argument("input_pyc", type=click.Path("r"), nargs=1)
210
+ @click.argument("input_pyc", type=click.Path(writable=True), nargs=1)
216
211
  @click.argument(
217
- "output_pyc", type=click.Path("w"), required=False, nargs=1, default=None
212
+ "output_pyc", type=click.Path(writable=True), required=False, nargs=1, default=None
218
213
  )
219
214
  def main(conversion_type, input_pyc, output_pyc):
220
215
  """Convert Python bytecode from one version to another.
221
216
 
222
217
  INPUT_PYC contains the input bytecode path name
223
- OUTPUT_PYC contians the output bytecode path name if supplied
218
+ OUTPUT_PYC contains the output bytecode path name if supplied
224
219
  The --conversion type option specifies what conversion to do.
225
220
 
226
221
  Note: there are a very limited set of conversions currently supported.
@@ -232,9 +227,9 @@ def main(conversion_type, input_pyc, output_pyc):
232
227
  src_version = conversion_to_version(conversion_type, is_dest=False)
233
228
  dest_version = conversion_to_version(conversion_type, is_dest=True)
234
229
  if output_pyc is None:
235
- output_pyc = "%s-%s.pyc" % (shortname, dest_version)
230
+ output_pyc = f"{shortname}-{dest_version}.pyc"
236
231
 
237
- if conversion_type in UPWARD_COMPATABLE:
232
+ if conversion_type in UPWARD_COMPATIBLE:
238
233
  copy_magic_into_pyc(input_pyc, output_pyc, src_version, dest_version)
239
234
  return
240
235
  temp_asm = NamedTemporaryFile("w", suffix=".pyasm", prefix=shortname, delete=False)
@@ -244,7 +239,7 @@ def main(conversion_type, input_pyc, output_pyc):
244
239
  temp_asm.close()
245
240
  assert version == float(
246
241
  src_version
247
- ), "Need Python %s bytecode; got bytecode for version %s" % (src_version, version)
242
+ ), f"Need Python {src_version} bytecode; got bytecode for version {version}"
248
243
  asm = asm_file(temp_asm.name)
249
244
  new_asm = transform_asm(asm, conversion_type, src_version, dest_version)
250
245
  os.unlink(temp_asm.name)
xasm/version.py CHANGED
@@ -5,4 +5,4 @@
5
5
  # space around "=" below.
6
6
 
7
7
  # fmt: off
8
- __version__="1.2.0" # noqa
8
+ __version__="1.2.1.dev0" # noqa
xasm/write_pyc.py CHANGED
@@ -1,14 +1,16 @@
1
+ import time
2
+ from struct import pack
3
+
1
4
  import xdis
2
5
  from xdis import magic2int
3
- from xdis.marsh import dumps
4
6
  from xdis.magics import magics
5
- from struct import pack
7
+ from xdis.marsh import dumps
6
8
  from xdis.version_info import PYTHON3, version_tuple_to_str
7
- import time
8
9
 
9
10
 
10
- def write_pycfile(fp, code_list, timestamp=None, version_triple=xdis.PYTHON_VERSION_TRIPLE):
11
+ def write_pycfile(fp, code_list, timestamp=None, version_triple=xdis.PYTHON_VERSION_TRIPLE) -> int:
11
12
 
13
+ rc = 0
12
14
  version_str = version_tuple_to_str(version_triple, end=2)
13
15
  magic_bytes = magics[version_str]
14
16
  magic_int = magic2int(magic_bytes)
@@ -40,5 +42,7 @@ def write_pycfile(fp, code_list, timestamp=None, version_triple=xdis.PYTHON_VERS
40
42
  pass
41
43
 
42
44
  fp.write(co_obj)
43
- except: # noqa
44
- pass
45
+ except Exception as e:
46
+ print(f"error dumping {co}: {e}; ignoring")
47
+ rc = 1
48
+ return rc
xasm/xasm_cli.py CHANGED
@@ -1,10 +1,14 @@
1
1
  #!/usr/bin/env python
2
- import click, os, sys
3
- from xasm.assemble import asm_file
4
- from xasm.write_pyc import write_pycfile
2
+ import os
3
+ import sys
4
+
5
+ import click
5
6
  import xdis
6
7
  from xdis.version_info import version_tuple_to_str
7
8
 
9
+ from xasm.assemble import asm_file
10
+ from xasm.write_pyc import write_pycfile
11
+
8
12
 
9
13
  @click.command()
10
14
  @click.option("--pyc-file", default=None)
@@ -23,12 +27,15 @@ def main(pyc_file, asm_path):
23
27
  for how to write a Python assembler file.
24
28
  """
25
29
  if os.stat(asm_path).st_size == 0:
26
- print("Size of assembly file %s is zero" % asm_path)
30
+ print(f"Size of assembly file {asm_path} is zero")
27
31
  sys.exit(1)
28
32
  asm = asm_file(asm_path)
29
33
 
30
- if not pyc_file and asm_path.endswith(".pyasm"):
31
- pyc_file = asm_path[: -len(".pyasm")] + ".pyc"
34
+ if not pyc_file:
35
+ if asm_path.endswith(".pyasm"):
36
+ pyc_file = asm_path[: -len(".pyasm")] + ".pyc"
37
+ elif not pyc_file and asm_path.endswith(".xasm"):
38
+ pyc_file = asm_path[: -len(".xasm")] + ".pyc"
32
39
 
33
40
  if xdis.PYTHON3:
34
41
  file_mode = "wb"
@@ -36,13 +43,17 @@ def main(pyc_file, asm_path):
36
43
  file_mode = "w"
37
44
 
38
45
  with open(pyc_file, file_mode) as fp:
39
- write_pycfile(fp, asm.code_list, asm.timestamp, asm.python_version)
46
+ rc = write_pycfile(fp, asm.code_list, asm.timestamp, asm.python_version)
40
47
  size = fp.tell()
41
48
  print(
42
49
  f"""Wrote Python {version_tuple_to_str(asm.python_version)} bytecode file "{pyc_file}"; {size} bytes."""
43
50
  )
44
51
  if size <= 16:
45
- print("Warning: bytecode file is too small to be usuable.")
52
+ print("Warning: bytecode file is too small to be usable.")
53
+ rc = 2
54
+ if rc != 0:
55
+ print(f"Exiting with return code {rc}")
56
+ sys.exit(rc)
46
57
 
47
58
 
48
59
  if __name__ == "__main__":
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: xasm
3
- Version: 1.2.0
3
+ Version: 1.2.1.dev0
4
4
  Summary: Python cross-version byte-code assembler
5
5
  Home-page: https://github.com/rocky/python-xasm/
6
6
  Author: Rocky Bernstein
@@ -19,9 +19,12 @@ Classifier: Programming Language :: Python :: 3.7
19
19
  Classifier: Programming Language :: Python :: 3.8
20
20
  Classifier: Programming Language :: Python :: 3.9
21
21
  Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
22
24
  Classifier: Topic :: Software Development :: Debuggers
23
25
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
26
  Description-Content-Type: text/x-rst
27
+ Requires-Dist: xdis (>=6.1.0)
25
28
 
26
29
  |Pypi Installs| |Latest Version| |Supported Python Versions|
27
30
 
@@ -30,36 +33,38 @@ xasm
30
33
 
31
34
  *NOTE: this is in beta*
32
35
 
33
- A Cross-Python bytecode Assembler
36
+ A cross-version Python bytecode assembler
34
37
 
35
38
 
36
39
  Introduction
37
40
  ------------
38
41
 
39
- The Python `xasm` module has routines for assembly, and has a command to
42
+ The Python ``xasm`` module has routines for assembly, and has a command to
40
43
  assemble bytecode for several different versions of Python.
41
44
 
42
45
  Here are some potential uses:
43
46
 
44
- * Make small patches to existing Python bytecode when you don’t have source
47
+ * Make small changes to existing Python bytecode when you don’t have source
45
48
  * Craft custom and efficient bytecode
46
49
  * Write an instruction-level optimizing compiler
47
50
  * Experiment with and learn about Python bytecode
48
- * Foil uncompyle6_ so that it can’t disassemble bytecode (at least for now)
51
+ * Foil decompilers like uncompyle6_ so that they can’t disassemble bytecode (at least for now)
49
52
 
50
- This will support bytecodes from Python version 1.0 to 3.8 or so.
53
+ This support the same kinds of bytecode that xdis_ supports. This is
54
+ pretty much all released bytecode before Python 3.11. We tend to lag behind the
55
+ latest Python releases.
51
56
 
52
- The code requires Python 2.7 or later.
57
+ The code requires Python 3.6 or later.
53
58
 
54
59
  Assembly files
55
60
  --------------
56
61
 
57
- More detail will be filled in, but some principles:
62
+ See how-to-use_ for more detail. Some general some principles:
58
63
 
59
- * Preferred extension for Python assembly is `.pyasm`
60
- * assembly is designed to work with the output of `pydisasm --asm`
64
+ * Preferred extension for Python assembly is ``.pyasm``
65
+ * assembly is designed to work with the output of ``pydisasm -F xasm``
61
66
  * Assembly file labels are at the beginning of the line
62
- and end in a colon, e.g. `END_IF`
67
+ and end in a colon, e.g. ``END_IF:``
63
68
  * instruction offsets in the assembly file are ignored and don't need
64
69
  to be entered
65
70
  * in those instructions that refer to offsets, if the if the
@@ -70,17 +75,30 @@ More detail will be filled in, but some principles:
70
75
  Installation
71
76
  ------------
72
77
 
73
- The standard Python routine:
78
+ *If you are using Python 3.11 or later*, you can install from PyPI using the name ``xasm``::
74
79
 
75
- ::
76
-
77
- pip install -e .
78
- pip install -r requirements-dev.txt
80
+ pip install xasm
79
81
 
80
- A GNU makefile is also provided so :code:`make install` (possibly as root or
82
+ A GNU makefile is also provided so ``make install`` (possibly as root or
81
83
  sudo) will do the steps above.
82
84
 
83
85
 
86
+ *If you are using Python before 3.11*, do not install using PyPI, but instead install using a file in the [GitHub Releases section](https://github.com/rocky/python-xasm/releases). Older Python used to use `easy_install <https://python101.pythonlibrary.org/chapter29_pip.html#using-easy-install>`_. But this is no longer supported in PyPi or newer Python versions. And vice versa, *poetry* nor *pip*, (the newer ways) are not supported on older Pythons.
87
+
88
+ If the Python version you are running xasm is between Python 3.6 through 3.11, use a tarball called xasm_36-*x.y.z*.tar.gz.
89
+
90
+ If the Python version you are running xasm is 3.11 or later, use a file called xasm-*x.y.z*.tar.gz.
91
+
92
+ Similarly, a tarball with or without the underscore *xx*, e.g., xasm_36-*x.y.z*.tar.gz. works only from Python 3.11 or greater.
93
+
94
+ Rationale for using Git Branches
95
+ ++++++++++++++++++++++++++++++++
96
+
97
+ It is currently impossible (if not impractical) to have one Python source code of this complexity and with this many features that can run both Python 3.6 and Python 3.13+. The languages have drifted so much, and packaging is vastly different.
98
+
99
+ A GNU makefile is also provided so :code:`make install` (possibly as root or sudo) will do the steps above.
100
+
101
+
84
102
  Testing
85
103
  -------
86
104
 
@@ -160,14 +178,14 @@ Here is an assembly for the above:
160
178
  RETURN_VALUE
161
179
 
162
180
 
163
- The above can be created automatically from Python source code using the `pydisasm`
164
- command from `xdis`:
181
+ The above can be created automatically from Python source code using the ``pydisasm``
182
+ command from ``xdis``:
165
183
 
166
184
  ::
167
185
 
168
186
  pydisasm --format xasm /tmp/five.pyc
169
187
 
170
- In the example above though, I have shortend and simplified the result.
188
+ In the example above though, I have shortened and simplified the result.
171
189
 
172
190
 
173
191
  Usage
@@ -180,7 +198,7 @@ To create a python bytecode file from an assemble file, run:
180
198
  pyc-xasm [OPTIONS] ASM_PATH
181
199
 
182
200
 
183
- For usage help, type `pyc-xasm --help`.
201
+ For usage help, type: ``pyc-xasm --help``.
184
202
 
185
203
 
186
204
  To convert a python bytecode from one bytecode to another, run:
@@ -190,7 +208,7 @@ To convert a python bytecode from one bytecode to another, run:
190
208
  pyc-convert [OPTIONS] INPUT_PYC [OUTPUT_PYC]
191
209
 
192
210
 
193
- For usage help, type `pyc-convert --help`.
211
+ For usage help, type: ``pyc-convert --help``.
194
212
 
195
213
 
196
214
  See Also
@@ -203,6 +221,8 @@ See Also
203
221
 
204
222
 
205
223
  .. _uncompyle6: https://github.com/rocky/python-uncompyle6
224
+ .. _how-to-use: https://github.com/rocky/python-xasm/blob/master/HOW-TO-USE.rst
225
+ .. _xdis: https://github.com/rocky/xdis
206
226
  .. |Latest Version| image:: https://badge.fury.io/py/xasm.svg
207
227
  :target: https://badge.fury.io/py/xasm
208
228
  .. |Pypi Installs| image:: https://pepy.tech/badge/xasm
@@ -0,0 +1,13 @@
1
+ xasm/__init__.py,sha256=gz94OZ_Jc1WozfOYH1pjQeg7IgB1L9wv5fyytJ2XgZU,174
2
+ xasm/assemble.py,sha256=gczajReJGCeCRGv7jZLWBsICZjzvJviXdMAwsRMOUWI,26257
3
+ xasm/pyc_convert.py,sha256=6XtMK0Q--KW1wQqSXGfKNSxyFAl5a--Q9jvRoALQRyk,9090
4
+ xasm/version.py,sha256=Fnb8SlddvYMcMFqKBfEsy6fBYxfxlGXDxelFtq6CwfM,211
5
+ xasm/write_pyc.py,sha256=ZoqjupN9rEjJ5o49SZcX9rbEhaImnqsJKkNzLnT9j4c,1455
6
+ xasm/xasm_cli.py,sha256=aGSypYaU2zotX-VXcVuZkPP45GzsQrlxGh0PwANBLK0,1754
7
+ xasm-1.2.1.dev0.dist-info/LICENSE.gpl2,sha256=2ylvL381vKOhdO-w6zkrOxe9lLNBhRQpo9_0EbHC_HM,18046
8
+ xasm-1.2.1.dev0.dist-info/METADATA,sha256=WRYfEEfTTLaHDOy2BgZ3P2dFvqoXCX6Y9oU52VDU7x4,7216
9
+ xasm-1.2.1.dev0.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
10
+ xasm-1.2.1.dev0.dist-info/entry_points.txt,sha256=ddG5Ud4yX9DHkME_98f0fEHp4X-_6nipBUq0IADDBfg,119
11
+ xasm-1.2.1.dev0.dist-info/top_level.txt,sha256=24UNOItB5o_EJoJ9nqhPZEC5GiSaYtHll3F6mJXzOkA,5
12
+ xasm-1.2.1.dev0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
13
+ xasm-1.2.1.dev0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.37.0)
2
+ Generator: bdist_wheel (0.37.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,13 +0,0 @@
1
- xasm/__init__.py,sha256=gz94OZ_Jc1WozfOYH1pjQeg7IgB1L9wv5fyytJ2XgZU,174
2
- xasm/assemble.py,sha256=SMLDowOTUyX5WW1F1MxVgVW8PABo0Q2i_qIQwrIA2VI,19603
3
- xasm/pyc_convert.py,sha256=onWRcmEhqpQi1Pf-lvPeun8qxPKmTWJEqLBGUW6aH28,9029
4
- xasm/version.py,sha256=DbW6-QjmrYtEr06qLRCM9LzccIv-54mBuJWsQ6fclI0,206
5
- xasm/write_pyc.py,sha256=LJKDKg8WGS9zzXIilY8keW3NJU2N2Kf_murjIVv1_IY,1357
6
- xasm/xasm_cli.py,sha256=cw2TY6kYOU50xC-czeLEGMBnCuYOjaV3mMuX75QKKnI,1514
7
- xasm-1.2.0.dist-info/LICENSE.gpl2,sha256=2ylvL381vKOhdO-w6zkrOxe9lLNBhRQpo9_0EbHC_HM,18046
8
- xasm-1.2.0.dist-info/METADATA,sha256=qIWC6CrrZ50SB9fpE38qoRfBgMGJ0S5HqwW--JUAKRw,5544
9
- xasm-1.2.0.dist-info/WHEEL,sha256=ewwEueio1C2XeHTvT17n8dZUJgOvyCWCt0WVNLClP9o,92
10
- xasm-1.2.0.dist-info/entry_points.txt,sha256=ddG5Ud4yX9DHkME_98f0fEHp4X-_6nipBUq0IADDBfg,119
11
- xasm-1.2.0.dist-info/top_level.txt,sha256=24UNOItB5o_EJoJ9nqhPZEC5GiSaYtHll3F6mJXzOkA,5
12
- xasm-1.2.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
13
- xasm-1.2.0.dist-info/RECORD,,