Skip to content
This repository was archived by the owner on Jan 15, 2021. It is now read-only.

Commit b349cae

Browse files
committed
Merge pull request #301 from autopulated/fasterfasterfaster
load subcommands lazily, and other speed improvements
2 parents 9a87aea + a054869 commit b349cae

File tree

5 files changed

+136
-59
lines changed

5 files changed

+136
-59
lines changed

bin/yotta

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#! /usr/bin/env python
2+
3+
import yotta
4+
yotta.main()
5+

bin/yt

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#! /usr/bin/env python
2+
3+
import yotta
4+
yotta.main()
5+

setup.py

+14-6
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,20 @@ def read(fname):
1313
# we need 'ntfsutils' in Windows
1414
if os.name == 'nt':
1515
platform_deps = ['ntfsutils>=0.1.3,<0.2']
16+
entry_points={
17+
"console_scripts": [
18+
"yotta=yotta:main",
19+
"yt=yotta:main",
20+
],
21+
}
22+
scripts = []
1623
else:
1724
platform_deps = []
25+
# entry points are nice, but add ~100ms to startup time with all the
26+
# pkg_resources infrastructure, so we use scripts= instead on unix-y
27+
# platforms:
28+
scripts = ['bin/yotta', 'bin/yt']
29+
entry_points = {}
1830

1931
setup(
2032
name = "yotta",
@@ -36,12 +48,8 @@ def read(fname):
3648
"License :: OSI Approved :: Apache Software License",
3749
"Environment :: Console",
3850
],
39-
entry_points={
40-
"console_scripts": [
41-
"yotta=yotta:main",
42-
"yt=yotta:main",
43-
],
44-
},
51+
entry_points=entry_points,
52+
scripts=scripts,
4553
test_suite = 'yotta.test',
4654
install_requires=[
4755
'semantic_version>=2.3.1,<3',

yotta/lib/lazyregex.py

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Copyright 2015 ARM Limited
2+
#
3+
# Licensed under the Apache License, Version 2.0
4+
# See LICENSE file for details.
5+
6+
# override re.compile so that it doesn't actually do any compilation until the
7+
# regex is first used. Many, many modules compile regexes when they are
8+
# imported, even if these are only needed for a subset of the module's
9+
# functionality, so this can significantly speed up importing modules:
10+
11+
import re
12+
13+
_original_re_compile = re.compile
14+
15+
class ReCompileProxy(object):
16+
def __init__(self, *args, **kwargs):
17+
self._args = args
18+
self._kwargs = kwargs
19+
self._real_obj = None
20+
21+
def __getattribute__(self, name):
22+
if object.__getattribute__(self, '_real_obj') is None:
23+
self._real_obj = _original_re_compile(
24+
*object.__getattribute__(self,'_args'),
25+
**object.__getattribute__(self,'_kwargs')
26+
)
27+
self._args = None
28+
self._kwargs = None
29+
return getattr(object.__getattribute__(self, '_real_obj'), name)
30+
31+
def overrideRECompile(*args, **kwargs):
32+
return ReCompileProxy(*args, **kwargs)
33+
34+
re.compile = overrideRECompile
35+

yotta/main.py

+77-53
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
# Licensed under the Apache License, Version 2.0
44
# See LICENSE file for details.
55

6+
from .lib import lazyregex
7+
68
# NOTE: argcomplete must be first!
79
# argcomplete, pip install argcomplete, tab-completion for argparse, Apache-2
810
import argcomplete
@@ -11,32 +13,8 @@
1113
import argparse
1214
import logging
1315
import sys
14-
import pkg_resources
1516
from functools import reduce
1617

17-
# subcommand modules, , add subcommands, internal
18-
from . import version
19-
from . import link
20-
from . import link_target
21-
from . import install
22-
from . import update
23-
from . import target
24-
from . import build
25-
from . import init
26-
from . import publish
27-
from . import unpublish
28-
from . import debug
29-
from . import test_subcommand as test
30-
from . import login
31-
from . import logout
32-
from . import list as list_command
33-
from . import uninstall
34-
from . import owners
35-
from . import licenses
36-
from . import clean
37-
from . import search
38-
from . import config
39-
4018
# logging setup, , setup the logging system, internal
4119
from .lib import logging_setup
4220
# detect, , detect things about the system, internal
@@ -54,6 +32,49 @@ def splitList(l, at_value):
5432
r[-1].append(x)
5533
return r
5634

35+
# (instead of subclassing argparse._SubParsersAction, we monkey-patch it,
36+
# because argcomplete has special-cases for the _SubParsersAction class that
37+
# it's impossible to extend to another class)
38+
#
39+
# add a add_parser_async function, which allows a subparser to be added whose
40+
# options are not evaluated until the subparser has been selected. This allows
41+
# us to defer loading the modules for all the subcommands until they are
42+
# actually:
43+
def _SubParsersAction_addParserAsync(self, name, *args, **kwargs):
44+
if not 'callback' in kwargs:
45+
raise ValueError('callback=fn(parser) argument must be specified')
46+
callback = kwargs['callback']
47+
del kwargs['callback']
48+
parser = self.add_parser(name, *args, **kwargs)
49+
parser._lazy_load_callback = callback
50+
return None
51+
argparse._SubParsersAction.add_parser_async = _SubParsersAction_addParserAsync
52+
53+
def _wrapSubParserActionCall(orig_call):
54+
def wrapped_call(self, parser, namespace, values, option_string=None):
55+
parser_name = values[0]
56+
if parser_name in self._name_parser_map:
57+
subparser = self._name_parser_map[parser_name]
58+
if hasattr(subparser, '_lazy_load_callback'):
59+
# the callback is responsible for adding the subparser's own
60+
# arguments:
61+
subparser._lazy_load_callback(subparser)
62+
# now we can go ahead and call the subparser action: its arguments are
63+
# now all set up
64+
return orig_call(self, parser, namespace, values, option_string)
65+
return wrapped_call
66+
argparse._SubParsersAction.__call__ = _wrapSubParserActionCall(argparse._SubParsersAction.__call__)
67+
68+
69+
# Override the argparse default version action so that we can avoid importing
70+
# pkg_resources (which is slowww) unless someone has actually asked for the
71+
# version
72+
class FastVersionAction(argparse.Action):
73+
def __call__(self, parser, args, values, option_string=None):
74+
import pkg_resources
75+
sys.stdout.write(pkg_resources.require("yotta")[0].version + '\n')
76+
sys.exit(0)
77+
5778

5879
def main():
5980
parser = argparse.ArgumentParser(
@@ -62,9 +83,8 @@ def main():
6283
'For more detailed help on each subcommand, run: yotta <subcommand> --help'
6384
)
6485
subparser = parser.add_subparsers(metavar='<subcommand>')
65-
66-
parser.add_argument('--version', dest='show_version', action='version',
67-
version=pkg_resources.require("yotta")[0].version,
86+
87+
parser.add_argument('--version', nargs=0, action=FastVersionAction,
6888
help='display the version'
6989
)
7090
parser.add_argument('-v', '--verbose', dest='verbosity', action='count',
@@ -92,25 +112,29 @@ def main():
92112
'--registry', default=None, dest='registry', help=argparse.SUPPRESS
93113
)
94114

95-
def addParser(name, module, description, help=None):
115+
def addParser(name, module_name, description, help=None):
96116
if help is None:
97117
help = description
98-
parser = subparser.add_parser(
118+
def onParserAdded(parser):
119+
import importlib
120+
module = importlib.import_module('.' + module_name, 'yotta')
121+
module.addOptions(parser)
122+
parser.set_defaults(command=module.execCommand)
123+
subparser.add_parser_async(
99124
name, description=description, help=help,
100-
formatter_class=argparse.RawTextHelpFormatter
125+
formatter_class=argparse.RawTextHelpFormatter,
126+
callback=onParserAdded
101127
)
102-
module.addOptions(parser)
103-
parser.set_defaults(command=module.execCommand)
104128

105-
addParser('search', search,
129+
addParser('search', 'search',
106130
'Search for open-source modules and targets that have been published '+
107131
'to the yotta registry (with yotta publish). See help for `yotta '+
108132
'install --save` for installing modules, and for `yotta target` for '+
109133
'switching targets.'
110134
)
111-
addParser('init', init, 'Create a new module.')
112-
addParser('install', install, 'Install dependencies for the current module, or a specific module.')
113-
addParser('build', build,
135+
addParser('init', 'init', 'Create a new module.')
136+
addParser('install', 'install', 'Install dependencies for the current module, or a specific module.')
137+
addParser('build', 'build',
114138
'Build the current module. Options can be passed to the underlying '+\
115139
'build tool by passing them after --, e.g. to do a parallel build '+\
116140
'when make is the build tool, run:\n yotta build -- -j\n\n'+
@@ -120,13 +144,13 @@ def addParser(name, module, description, help=None):
120144
'all dependencies, run:\n yotta build all_tests\n\n',
121145
'Build the current module.'
122146
)
123-
addParser('version', version, 'Bump the module version, or (with no arguments) display the current version.')
124-
addParser('link', link, 'Symlink a module.')
125-
addParser('link-target', link_target, 'Symlink a target.')
126-
addParser('update', update, 'Update dependencies for the current module, or a specific module.')
127-
addParser('target', target, 'Set or display the target device.')
128-
addParser('debug', debug, 'Attach a debugger to the current target. Requires target support.')
129-
addParser('test', test,
147+
addParser('version', 'version', 'Bump the module version, or (with no arguments) display the current version.')
148+
addParser('link', 'link', 'Symlink a module.')
149+
addParser('link-target', 'link_target', 'Symlink a target.')
150+
addParser('update', 'update', 'Update dependencies for the current module, or a specific module.')
151+
addParser('target', 'target', 'Set or display the target device.')
152+
addParser('debug', 'debug', 'Attach a debugger to the current target. Requires target support.')
153+
addParser('test', 'test_subcommand',
130154
'Run the tests for the current module on the current target. A build '+
131155
'will be run first, and options to the build subcommand are also '+
132156
'accepted by test.\nThis subcommand requires the target to provide a '+
@@ -135,16 +159,16 @@ def addParser(name, module, description, help=None):
135159
'each test, and may produce a summary.',
136160
'Run the tests for the current module on the current target. Requires target support.'
137161
)
138-
addParser('publish', publish, 'Publish a module or target to the public registry.')
139-
addParser('unpublish', unpublish, 'Un-publish a recently published module or target.')
140-
addParser('login', login, 'Authorize for access to private github repositories and publishing to the yotta registry.')
141-
addParser('logout', logout, 'Remove saved authorization token for the current user.')
142-
addParser('list', list_command, 'List the dependencies of the current module, or the inherited targets of the current target.')
143-
addParser('uninstall', uninstall, 'Remove a specific dependency of the current module.')
144-
addParser('owners', owners, 'Add/remove/display the owners of a module or target.')
145-
addParser('licenses', licenses, 'List the licenses of the current module and its dependencies.')
146-
addParser('clean', clean, 'Remove files created by yotta and the build.')
147-
addParser('config', config, 'Display the target configuration info.')
162+
addParser('publish', 'publish', 'Publish a module or target to the public registry.')
163+
addParser('unpublish', 'unpublish', 'Un-publish a recently published module or target.')
164+
addParser('login', 'login', 'Authorize for access to private github repositories and publishing to the yotta registry.')
165+
addParser('logout', 'logout', 'Remove saved authorization token for the current user.')
166+
addParser('list', 'list', 'List the dependencies of the current module, or the inherited targets of the current target.')
167+
addParser('uninstall', 'uninstall', 'Remove a specific dependency of the current module.')
168+
addParser('owners', 'owners', 'Add/remove/display the owners of a module or target.')
169+
addParser('licenses', 'licenses', 'List the licenses of the current module and its dependencies.')
170+
addParser('clean', 'clean', 'Remove files created by yotta and the build.')
171+
addParser('config', 'config', 'Display the target configuration info.')
148172

149173
# short synonyms, subparser.choices is a dictionary, so use update() to
150174
# merge in the keys from another dictionary

0 commit comments

Comments
 (0)