Skip to content

Commit 43df3c8

Browse files
authored
Merge pull request #52 from llllllllll/test-coverage
Test coverage
2 parents 77a97a7 + 2e04dc9 commit 43df3c8

File tree

8 files changed

+351
-82
lines changed

8 files changed

+351
-82
lines changed

.coveragerc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[run]
2+
omit =
3+
codetransformer/_version.py

codetransformer/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .code import Code
1+
from .code import Code, Flag
22
from .core import CodeTransformer
33
from . patterns import (
44
matchany,
@@ -20,7 +20,7 @@
2020
del get_versions
2121

2222

23-
def load_ipython_extension(ipython):
23+
def load_ipython_extension(ipython): # pragma: no cover
2424

2525
def dis_magic(line, cell=None):
2626
if cell is None:
@@ -41,6 +41,7 @@ def ast_magic(line, cell=None):
4141
'display',
4242
'Code',
4343
'CodeTransformer',
44+
'Flag',
4445
'instructions',
4546
'matchany',
4647
'not_',

codetransformer/code.py

Lines changed: 67 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,46 @@ def _freevar_argname(arg, cellvars, freevars):
199199
return freevars[arg - len_cellvars]
200200

201201

202+
def pycode(argcount,
203+
kwonlyargcount,
204+
nlocals,
205+
stacksize,
206+
flags,
207+
codestring,
208+
constants,
209+
names,
210+
varnames,
211+
filename,
212+
name,
213+
firstlineno,
214+
lnotab,
215+
freevars=(),
216+
cellvars=()):
217+
"""types.CodeType constructor that accepts keyword arguments.
218+
219+
See Also
220+
--------
221+
types.CodeType
222+
"""
223+
return CodeType(
224+
argcount,
225+
kwonlyargcount,
226+
nlocals,
227+
stacksize,
228+
flags,
229+
codestring,
230+
constants,
231+
names,
232+
varnames,
233+
filename,
234+
name,
235+
firstlineno,
236+
lnotab,
237+
freevars,
238+
cellvars,
239+
)
240+
241+
202242
class Code:
203243
"""A higher abstraction over python's CodeType.
204244
@@ -218,14 +258,8 @@ class Code:
218258
The first line number of the code in this code object.
219259
lnotab : dict[Instruction -> int], optional
220260
The mapping from instruction to the line that it starts.
221-
nested : bool, optional
222-
Is this code object nested in another code object?
223-
coroutine : bool, optional
224-
Is this code object a coroutine (async def)?
225-
iterable_coroutine : bool, optional
226-
Is this code object a coroutine iterator?
227-
new_locals : bool, optional
228-
Should this code object construct new locals?
261+
flags : dict[str -> bool], optional
262+
Any flags to set. This updates the default flag set.
229263
230264
Attributes
231265
----------
@@ -276,10 +310,7 @@ def __init__(self,
276310
filename='<code>',
277311
firstlineno=1,
278312
lnotab=None,
279-
nested=False,
280-
coroutine=False,
281-
iterable_coroutine=False,
282-
new_locals=False):
313+
flags=None):
283314

284315
instrs = tuple(instrs) # strictly evaluate any generators.
285316

@@ -335,27 +366,30 @@ def __init__(self,
335366
self._filename = filename
336367
self._firstlineno = firstlineno
337368
self._lnotab = lnotab or {}
338-
self._flags = Flag.pack(
339-
CO_OPTIMIZED=True,
340-
CO_NEWLOCALS=new_locals,
341-
CO_VARARGS=varg is not None,
342-
CO_VARKEYWORDS=kwarg is not None,
343-
CO_NESTED=nested,
344-
CO_GENERATOR=any(
345-
isinstance(instr, (YIELD_VALUE, YIELD_FROM))
346-
for instr in instrs
369+
self._flags = Flag.pack(**dict(
370+
dict(
371+
CO_OPTIMIZED=True,
372+
CO_NEWLOCALS=True,
373+
CO_VARARGS=varg is not None,
374+
CO_VARKEYWORDS=kwarg is not None,
375+
CO_NESTED=False,
376+
CO_GENERATOR=any(
377+
isinstance(instr, (YIELD_VALUE, YIELD_FROM))
378+
for instr in instrs
379+
),
380+
CO_NOFREE=not any(map(op.attrgetter('uses_free'), instrs)),
381+
CO_COROUTINE=False,
382+
CO_ITERABLE_COROUTINE=False,
383+
CO_FUTURE_DIVISION=False,
384+
CO_FUTURE_ABSOLUTE_IMPORT=False,
385+
CO_FUTURE_WITH_STATEMENT=False,
386+
CO_FUTURE_PRINT_FUNCTION=False,
387+
CO_FUTURE_UNICODE_LITERALS=False,
388+
CO_FUTURE_BARRY_AS_BDFL=False,
389+
CO_FUTURE_GENERATOR_STOP=False,
347390
),
348-
CO_NOFREE=not any(map(op.attrgetter('uses_free'), instrs)),
349-
CO_COROUTINE=coroutine,
350-
CO_ITERABLE_COROUTINE=iterable_coroutine,
351-
CO_FUTURE_DIVISION=False,
352-
CO_FUTURE_ABSOLUTE_IMPORT=False,
353-
CO_FUTURE_WITH_STATEMENT=False,
354-
CO_FUTURE_PRINT_FUNCTION=False,
355-
CO_FUTURE_UNICODE_LITERALS=False,
356-
CO_FUTURE_BARRY_AS_BDFL=False,
357-
CO_FUTURE_GENERATOR_STOP=False,
358-
)
391+
**flags or {}
392+
))
359393

360394
@classmethod
361395
def from_pyfunc(cls, f):
@@ -454,10 +488,7 @@ def from_pycode(cls, co):
454488
lnotab={
455489
lno: sparse_instrs[off] for off, lno in findlinestarts(co)
456490
},
457-
nested=flags['CO_NESTED'],
458-
coroutine=flags['CO_COROUTINE'],
459-
iterable_coroutine=flags['CO_ITERABLE_COROUTINE'],
460-
new_locals=flags['CO_NEWLOCALS'],
491+
flags=flags,
461492
)
462493

463494
def to_pycode(self):

codetransformer/core.py

Lines changed: 67 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
from ctypes import py_object, pythonapi
44
from itertools import chain
55
from types import CodeType, FunctionType
6+
from weakref import WeakKeyDictionary
7+
8+
try:
9+
import threading
10+
except ImportError:
11+
import dummy_threading as threading
612

713
from .code import Code
814
from .instructions import LOAD_CONST, STORE_FAST, LOAD_FAST
@@ -11,6 +17,7 @@
1117
patterndispatcher,
1218
DEFAULT_STARTCODE,
1319
)
20+
from .utils.instance import instance
1421

1522

1623
_cell_new = pythonapi.PyCell_New
@@ -48,7 +55,18 @@ class NoContext(Exception):
4855
attribute was accessed outside of a code context.
4956
"""
5057
def __init__(self):
51-
return super().__init__('no context')
58+
return super().__init__('no active transformation context')
59+
60+
61+
class Context:
62+
"""Empty object for holding the transformation context.
63+
"""
64+
def __init__(self, code):
65+
self.code = code
66+
self.startcode = DEFAULT_STARTCODE
67+
68+
def __repr__(self): # pragma: no cover
69+
return '<%s: %r>' % (type(self).__name__, self.__dict__)
5270

5371

5472
class CodeTransformerMeta(type):
@@ -81,11 +99,7 @@ class CodeTransformer(metaclass=CodeTransformerMeta):
8199
----------
82100
code
83101
"""
84-
__slots__ = '_code_stack', '_startcode_stack'
85-
86-
def __init__(self):
87-
self._code_stack = []
88-
self._startcode_stack = []
102+
__slots__ = '__weakref__',
89103

90104
def transform_consts(self, consts):
91105
"""transformer for the co_consts field.
@@ -186,9 +200,7 @@ def transform(self, code, *, name=None, filename=None):
186200
filename=filename if filename is not None else code.filename,
187201
firstlineno=code.firstlineno,
188202
lnotab=_new_lnotab(post_transform, code.lnotab),
189-
nested=code.is_nested,
190-
coroutine=code.is_coroutine,
191-
iterable_coroutine=code.is_iterable_coroutine,
203+
flags=code.flags,
192204
)
193205

194206
def __call__(self, f, *,
@@ -207,33 +219,67 @@ def __call__(self, f, *,
207219
closure,
208220
)
209221

222+
@instance
223+
class _context_stack(threading.local):
224+
"""Thread safe transformation context stack.
225+
226+
Each thread will get it's own ``WeakKeyDictionary`` that maps
227+
instances to a stack of ``Context`` objects. When this descriptor
228+
is looked up we first try to get the weakkeydict off of the thread
229+
local storage. If it doesn't exist we make a new map. Then we lookup
230+
our instance in this map. If it doesn't exist yet create a new stack
231+
(as an empty list).
232+
233+
This allows a single instance of ``CodeTransformer`` to be used
234+
recursively to transform code objects in a thread safe way while
235+
still being able to use a stateful context.
236+
"""
237+
def __get__(self, instance, owner):
238+
try:
239+
stacks = self._context_stacks
240+
except AttributeError:
241+
stacks = self._context_stacks = WeakKeyDictionary()
242+
243+
if instance is None:
244+
# when looked up off the class return the current threads
245+
# context stacks map
246+
return stacks
247+
248+
return stacks.setdefault(instance, [])
249+
210250
@contextmanager
211251
def _new_context(self, code):
212-
self._code_stack.append(code)
213-
self._startcode_stack.append(DEFAULT_STARTCODE)
252+
self._context_stack.append(Context(code))
214253
try:
215254
yield
216255
finally:
217-
self._code_stack.pop()
218-
self._startcode_stack.pop()
256+
self._context_stack.pop()
219257

220258
@property
221-
def code(self):
222-
"""The code object we are currently manipulating.
259+
def context(self):
260+
"""Lookup the current transformation context.
261+
262+
Raises
263+
------
264+
NoContext
265+
Raised when there is no active transformation context.
223266
"""
224267
try:
225-
return self._code_stack[-1]
268+
return self._context_stack[-1]
226269
except IndexError:
227270
raise NoContext()
228271

272+
@property
273+
def code(self):
274+
"""The code object we are currently manipulating.
275+
"""
276+
return self.context.code
277+
229278
@property
230279
def startcode(self):
231280
"""The startcode we are currently in.
232281
"""
233-
try:
234-
return self._startcode_stack[-1]
235-
except IndexError:
236-
raise NoContext()
282+
return self.context.startcode
237283

238284
def begin(self, startcode):
239285
"""Begin a new startcode.
@@ -243,9 +289,4 @@ def begin(self, startcode):
243289
startcode : any
244290
The startcode to begin.
245291
"""
246-
try:
247-
# "beginning" a new startcode changes the current startcode.
248-
# Here we are mutating the current context's startcode.
249-
self._startcode_stack[-1] = startcode
250-
except IndexError:
251-
raise NoContext()
292+
self.context.startcode = startcode

codetransformer/instructions.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
IntEnum,
55
unique,
66
)
7+
from operator import attrgetter
78
from re import escape
89

910
from .patterns import matchable
@@ -13,7 +14,7 @@
1314

1415
__all__ = ['Instruction'] + sorted(list(opmap))
1516

16-
# The opcodes that use the co_names tuple.
17+
# The instructions that use the co_names tuple.
1718
_uses_name = frozenset({
1819
'DELETE_ATTR',
1920
'DELETE_GLOBAL',
@@ -27,13 +28,13 @@
2728
'STORE_GLOBAL',
2829
'STORE_NAME',
2930
})
30-
# The opcodes that use the co_varnames tuple.
31+
# The instructions that use the co_varnames tuple.
3132
_uses_varname = frozenset({
3233
'LOAD_FAST',
3334
'STORE_FAST',
3435
'DELETE_FAST',
3536
})
36-
# The opcodes that use the free vars.
37+
# The instructions that use the co_freevars tuple.
3738
_uses_free = frozenset({
3839
'DELETE_DEREF',
3940
'LOAD_CLASSDEREF',
@@ -430,3 +431,16 @@ def __get__(self, instance, owner):
430431
del _check_jmp_arg
431432
del _call_repr
432433
del _mk_call_init
434+
435+
# The instructions that use the co_names tuple.
436+
uses_name = frozenset(
437+
filter(attrgetter('uses_name'), Instruction.__subclasses__()),
438+
)
439+
# The instructions that use the co_varnames tuple.
440+
uses_varname = frozenset(
441+
filter(attrgetter('uses_varname'), Instruction.__subclasses__()),
442+
)
443+
# The instructions that use the co_freevars tuple.
444+
uses_free = frozenset(
445+
filter(attrgetter('uses_free'), Instruction.__subclasses__()),
446+
)

0 commit comments

Comments
 (0)