Skip to content

Commit b8d3f86

Browse files
authored
Merge pull request #1328 from moreati/issue1325-scan_code_imports-refactor
mitogen: Refactor `mitogen.master.scan_code_imports()` -> `mitogen.imports.codeobj_imports()`
2 parents 1386529 + 0e5f47f commit b8d3f86

File tree

18 files changed

+2425
-109
lines changed

18 files changed

+2425
-109
lines changed

ansible_mitogen/module_finder.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
# Python < 3.4, PEP 302 Import Hooks
4545
import imp
4646

47-
import mitogen.master
47+
import mitogen.imports
4848

4949

5050
LOG = logging.getLogger(__name__)
@@ -146,7 +146,7 @@ def scan_fromlist(code):
146146
>>> list(scan_fromlist(code))
147147
[(0, 'a'), (0, 'b.c'), (0, 'd.e.f'), (0, 'g.h'), (0, 'g.i')]
148148
"""
149-
for level, modname_s, fromlist in mitogen.master.scan_code_imports(code):
149+
for level, modname_s, fromlist in mitogen.imports.codeobj_imports(code):
150150
for name in fromlist:
151151
yield level, str('%s.%s' % (modname_s, name))
152152
if not fromlist:
@@ -172,7 +172,7 @@ def walk_imports(code, prefix=None):
172172
prefix = ''
173173
pattern = re.compile(r'(^|\.)(\w+)')
174174
start = len(prefix)
175-
for _, name, fromlist in mitogen.master.scan_code_imports(code):
175+
for _, name, fromlist in mitogen.imports.codeobj_imports(code):
176176
if not name.startswith(prefix):
177177
continue
178178
for match in pattern.finditer(name, start):

docs/changelog.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,12 @@ To avail of fixes in an unreleased version, please download a ZIP file
2121
In progress (unreleased)
2222
------------------------
2323

24+
* :gh:issue:`1325` :mod:`mitogen`: Refactor
25+
``mitogen.master.scan_code_imports()`` as
26+
:func:`mitogen.import.codeobj_imports` and speed-up by 1.5 - 2.5 x
2427
* :gh:issue:`1329` CI: Refactor and de-duplicate Github Actions workflow
2528
* :gh:issue:`1315` CI: macOS: Increase failed logins limit of test users
29+
* :gh:issue:`1325` tests: Improve ``master_test.ScanCodeImportsTest`` coverage
2630

2731

2832
v0.3.26 (2025-08-04)

mitogen/core.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1300,6 +1300,7 @@ class Importer(object):
13001300
'kubectl',
13011301
'fakessh',
13021302
'fork',
1303+
'imports',
13031304
'jail',
13041305
'lxc',
13051306
'lxd',

mitogen/imports/__init__.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# SPDX-FileCopyrightText: 2025 Mitogen authors <https://github.com/mitogen-hq>
2+
# SPDX-License-Identifier: MIT
3+
# !mitogen: minify_safe
4+
5+
import sys
6+
7+
if sys.version_info >= (3, 6):
8+
from mitogen.imports._py36 import _code_imports
9+
elif sys.version_info >= (2, 5):
10+
from mitogen.imports._py2 import _code_imports_py25 as _code_imports
11+
else:
12+
from mitogen.imports._py2 import _code_imports_py24 as _code_imports
13+
14+
15+
def codeobj_imports(co):
16+
"""
17+
Yield (level, modname, names) tuples by scanning the code object `co`.
18+
19+
Top level `import mod` & `from mod import foo` statements are matched.
20+
Those inside a `class ...` or `def ...` block are currently skipped.
21+
22+
>>> co = compile('import a, b; from c import d, e as f', '<str>', 'exec')
23+
>>> list(codeobj_imports(co)) # doctest: +ELLIPSIS
24+
[(..., 'a', ()), (..., 'b', ()), (..., 'c', ('d', 'e'))]
25+
26+
:return:
27+
Generator producing `(level, modname, names)` tuples, where:
28+
29+
* `level`:
30+
-1 implicit relative (Python 2.x default)
31+
0 absolute (Python 3.x, `from __future__ import absolute_import`)
32+
>0 explicit relative (`from . import a`, `from ..b, import c`)
33+
* `modname`: Name of module to import, or to import `names` from.
34+
* `names`: tuple of names in `from mod import ..`.
35+
"""
36+
return _code_imports(co.co_code, co.co_consts, co.co_names)

mitogen/imports/_py2.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# SPDX-FileCopyrightText: 2025 Mitogen authors <https://github.com/mitogen-hq>
2+
# SPDX-License-Identifier: MIT
3+
# !mitogen: minify_safe
4+
5+
import array
6+
import itertools
7+
import opcode
8+
9+
10+
IMPORT_NAME = opcode.opmap['IMPORT_NAME']
11+
LOAD_CONST = opcode.opmap['LOAD_CONST']
12+
13+
14+
def _opargs(code, _have_arg=opcode.HAVE_ARGUMENT):
15+
it = iter(array.array('B', code))
16+
nexti = it.next
17+
for i in it:
18+
if i >= _have_arg:
19+
yield (i, nexti() | (nexti() << 8))
20+
else:
21+
yield (i, None)
22+
23+
24+
def _code_imports_py25(code, consts, names):
25+
it1, it2, it3 = itertools.tee(_opargs(code), 3)
26+
try:
27+
next(it2)
28+
next(it3)
29+
next(it3)
30+
except StopIteration:
31+
return
32+
for oparg1, oparg2, (op3, arg3) in itertools.izip(it1, it2, it3):
33+
if op3 != IMPORT_NAME:
34+
continue
35+
op1, arg1 = oparg1
36+
op2, arg2 = oparg2
37+
if op1 != LOAD_CONST or op2 != LOAD_CONST:
38+
continue
39+
yield (consts[arg1], names[arg3], consts[arg2] or ())
40+
41+
42+
def _code_imports_py24(code, consts, names):
43+
it1, it2 = itertools.tee(_opargs(code), 2)
44+
try:
45+
next(it2)
46+
except StopIteration:
47+
return
48+
for oparg1, (op2, arg2) in itertools.izip(it1, it2):
49+
if op2 != IMPORT_NAME:
50+
continue
51+
op1, arg1 = oparg1
52+
if op1 != LOAD_CONST:
53+
continue
54+
yield (-1, names[arg2], consts[arg1] or ())

mitogen/imports/_py36.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# SPDX-FileCopyrightText: 2025 Mitogen authors <https://github.com/mitogen-hq>
2+
# SPDX-License-Identifier: MIT
3+
# !mitogen: minify_safe
4+
5+
import opcode
6+
7+
IMPORT_NAME = opcode.opmap['IMPORT_NAME']
8+
LOAD_CONST = opcode.opmap['LOAD_CONST']
9+
10+
11+
def _code_imports(code, consts, names):
12+
start = 4
13+
while True:
14+
op3_idx = code.find(IMPORT_NAME, start, -1)
15+
if op3_idx < 0:
16+
return
17+
if op3_idx % 2:
18+
start = op3_idx + 1
19+
continue
20+
if code[op3_idx-4] != LOAD_CONST or code[op3_idx-2] != LOAD_CONST:
21+
start = op3_idx + 2
22+
continue
23+
start = op3_idx + 6
24+
arg1, arg2, arg3 = code[op3_idx-3], code[op3_idx-1], code[op3_idx+1]
25+
yield (consts[arg1], names[arg3], consts[arg2] or ())

mitogen/master.py

Lines changed: 2 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,8 @@
3535
contexts.
3636
"""
3737

38-
import dis
3938
import errno
4039
import inspect
41-
import itertools
4240
import logging
4341
import os
4442
import pkgutil
@@ -83,21 +81,18 @@ def _find_loader(fullname):
8381

8482
import mitogen
8583
import mitogen.core
84+
import mitogen.imports
8685
import mitogen.minify
8786
import mitogen.parent
8887

8988
from mitogen.core import any
9089
from mitogen.core import b
9190
from mitogen.core import IOLOG
9291
from mitogen.core import LOG
93-
from mitogen.core import next
9492
from mitogen.core import str_partition
9593
from mitogen.core import str_rpartition
9694
from mitogen.core import to_text
9795

98-
imap = getattr(itertools, 'imap', map)
99-
izip = getattr(itertools, 'izip', zip)
100-
10196
RLOG = logging.getLogger('mitogen.ctx')
10297

10398

@@ -253,80 +248,6 @@ def _get_core_source():
253248
mitogen.parent._get_core_source = _get_core_source
254249

255250

256-
LOAD_CONST = dis.opname.index('LOAD_CONST')
257-
IMPORT_NAME = dis.opname.index('IMPORT_NAME')
258-
259-
260-
def _getarg(nextb, c):
261-
if c >= dis.HAVE_ARGUMENT:
262-
return nextb() | (nextb() << 8)
263-
264-
265-
if sys.version_info < (3, 0):
266-
def iter_opcodes(co):
267-
# Yield `(op, oparg)` tuples from the code object `co`.
268-
ordit = imap(ord, co.co_code)
269-
nextb = ordit.next
270-
return ((c, _getarg(nextb, c)) for c in ordit)
271-
elif sys.version_info < (3, 6):
272-
def iter_opcodes(co):
273-
# Yield `(op, oparg)` tuples from the code object `co`.
274-
ordit = iter(co.co_code)
275-
nextb = ordit.__next__
276-
return ((c, _getarg(nextb, c)) for c in ordit)
277-
else:
278-
def iter_opcodes(co):
279-
# Yield `(op, oparg)` tuples from the code object `co`.
280-
ordit = iter(co.co_code)
281-
nextb = ordit.__next__
282-
# https://github.com/abarnert/cpython/blob/c095a32f/Python/wordcode.md
283-
return ((c, nextb()) for c in ordit)
284-
285-
286-
def scan_code_imports(co):
287-
"""
288-
Given a code object `co`, scan its bytecode yielding any ``IMPORT_NAME``
289-
and associated prior ``LOAD_CONST`` instructions representing an `Import`
290-
statement or `ImportFrom` statement.
291-
292-
:return:
293-
Generator producing `(level, modname, namelist)` tuples, where:
294-
295-
* `level`: -1 for normal import, 0, for absolute import, and >0 for
296-
relative import.
297-
* `modname`: Name of module to import, or from where `namelist` names
298-
are imported.
299-
* `namelist`: for `ImportFrom`, the list of names to be imported from
300-
`modname`.
301-
"""
302-
opit = iter_opcodes(co)
303-
opit, opit2, opit3 = itertools.tee(opit, 3)
304-
305-
try:
306-
next(opit2)
307-
next(opit3)
308-
next(opit3)
309-
except StopIteration:
310-
return
311-
312-
if sys.version_info >= (2, 5):
313-
for oparg1, oparg2, (op3, arg3) in izip(opit, opit2, opit3):
314-
if op3 == IMPORT_NAME:
315-
op2, arg2 = oparg2
316-
op1, arg1 = oparg1
317-
if op1 == op2 == LOAD_CONST:
318-
yield (co.co_consts[arg1],
319-
co.co_names[arg3],
320-
co.co_consts[arg2] or ())
321-
else:
322-
# Python 2.4 did not yet have 'level', so stack format differs.
323-
for oparg1, (op2, arg2) in izip(opit, opit2):
324-
if op2 == IMPORT_NAME:
325-
op1, arg1 = oparg1
326-
if op1 == LOAD_CONST:
327-
yield (-1, co.co_names[arg2], co.co_consts[arg1] or ())
328-
329-
330251
class ThreadWatcher(object):
331252
"""
332253
Manage threads that wait for another thread to shut down, before invoking
@@ -1029,7 +950,7 @@ def find_related_imports(self, fullname):
1029950
maybe_names = list(self.generate_parent_names(fullname))
1030951

1031952
co = compile(src, modpath, 'exec')
1032-
for level, modname, namelist in scan_code_imports(co):
953+
for level, modname, namelist in mitogen.imports.codeobj_imports(co):
1033954
if level == -1:
1034955
modnames = [modname, '%s.%s' % (fullname, modname)]
1035956
else:

0 commit comments

Comments
 (0)