Skip to content

Commit 15793ec

Browse files
committed
DEV: Adds the ability to use config files instead of passing long lists of
commands. Users may now put a .qdb in the cwd and/or in their home to create a configuration to pass to Qdb when it is started.
1 parent 8b691e6 commit 15793ec

File tree

6 files changed

+349
-69
lines changed

6 files changed

+349
-69
lines changed

.qdb

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Example .qdb file.
2+
config = QdbConfig(**{
3+
'host': 'localhost',
4+
'port': 8001,
5+
'auth_msg': '',
6+
'default_file': None,
7+
'default_namespace': None,
8+
'eval_fn': None,
9+
'exception_serializer': None,
10+
'skip_fn': None,
11+
'pause_signal': None,
12+
'redirect_output': True,
13+
'retry_attepts': 10,
14+
'uuid': 'qdb',
15+
'cmd_manager': None,
16+
'green': False,
17+
'repr_fn': None,
18+
'log_file': None,
19+
'execution_timeout': None,
20+
})

etc/requirements.txt

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ gevent-websocket==0.9.3
44
greenlet==0.4.2
55
gipc==0.4.0
66

7+
six==1.7.3
8+
79
contextlib2==0.4.0
810

911
Logbook==0.7.0

qdb/config.py

+167
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
#
2+
# Copyright 2014 Quantopian, Inc.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
from collections import namedtuple
16+
from itertools import chain, repeat
17+
import os
18+
19+
from logbook import Logger
20+
from six.moves import zip, reduce
21+
22+
23+
def _coerce_dict(dict_like):
24+
if isinstance(dict_like, QdbConfig):
25+
return dict_like._asdict()
26+
return dict_like
27+
28+
29+
log = Logger('qdb_config')
30+
31+
32+
DEFAULT_OPTIONS = {
33+
'host': 'localhost',
34+
'port': 8001,
35+
'auth_msg': '',
36+
'default_file': None,
37+
'default_namespace': None,
38+
'eval_fn': None,
39+
'exception_serializer': None,
40+
'skip_fn': None,
41+
'pause_signal': None,
42+
'redirect_output': True,
43+
'retry_attepts': 10,
44+
'uuid': 'qdb',
45+
'cmd_manager': None,
46+
'green': False,
47+
'repr_fn': None,
48+
'log_file': None,
49+
'execution_timeout': None,
50+
}
51+
52+
53+
class QdbConfig(namedtuple('QdbConfig', DEFAULT_OPTIONS)):
54+
"""
55+
Qdb configuration.
56+
"""
57+
filename = '.qdb'
58+
DEFAULT_OPTIONS = DEFAULT_OPTIONS
59+
60+
kwargs_first = 1
61+
config_first = -1
62+
63+
def __new__(cls, **kwargs):
64+
"""
65+
A structure to hold the arguments to pass to Qdb.
66+
67+
Args:
68+
host (str): The `host` of the server.
69+
port (int): The `port` to connect on.
70+
auth_msg (str): A message that will be sent with the start event
71+
to the server. This can be used to do server/tracer authentication.
72+
default_file (str): a file to use if the file field is ommited from
73+
payloads.
74+
eval_fn (function): The function to eval code where the user may
75+
provide evaluate anything. For example in a conditional breakpoint
76+
or in the repl.
77+
exception_serializer (function): The function to convert exceptions
78+
into strings to send back to the user.
79+
skip_fn (function): Simmilar to the skip List feature of Bdb, except
80+
that it should be a function that takes a filename and returns True
81+
iff the debugger should skip this file. These files will be
82+
suppressed from stack traces.
83+
pause_signal (int): Signal to raise in this program to trigger a
84+
pause command. If this is none, this will default to SIGUSR2.
85+
retry_attempts (int): The number of times to attempt to connect to
86+
the server before raising a QdbFailedToConnect error.
87+
uuid (str): The identifier on the server for this session. If none is
88+
provided, it will generate a uuid4.
89+
cmd_manager (subclass of CommandManager): A callable that takes a Qdb
90+
instance and manages commands by implementing a next_command
91+
method. If none, a new, default manager will be created that reads
92+
commands from the server at (`host`, `port`).
93+
green (bool): If True, this will use gevent safe timeouts, otherwise
94+
this will use signal based timeouts.
95+
repr_fn (function): A function to use to convert objects to strings
96+
to send then back to the server. By default, this wraps repr by
97+
catching exceptions and reporting them to the user.
98+
log_file (str): The file to log to, if None, log to stderr.
99+
execution_timeout (int): The amount of time user code has to execute
100+
before being cut short. This is applied to the repl, watchlist and
101+
conditional breakpoints. If None, no timeout is applied.
102+
"""
103+
extra = [k for k in kwargs if k not in cls.DEFAULT_OPTIONS]
104+
if extra:
105+
raise TypeError('QdbConfig received extra args: %s' % extra)
106+
107+
options = dict(cls.DEFAULT_OPTIONS)
108+
options.update(kwargs)
109+
return super(QdbConfig, cls).__new__(cls, **options)
110+
111+
@classmethod
112+
def read_from_file(cls, filepath):
113+
namespace = {}
114+
try:
115+
with open(filepath, 'r') as f:
116+
exec(f.read(), {cls.__name__: cls}, namespace)
117+
except IOError:
118+
# Ignore missing files
119+
log.debug('Skipping loading config from: %s' % filepath)
120+
121+
return namespace.get('config')
122+
123+
@classmethod
124+
def get_config(cls,
125+
config=None,
126+
files=None,
127+
use_local=True,
128+
use_profile=True):
129+
"""
130+
Gets a config, checking the project local config, the
131+
user-set profile, and any addition files.
132+
"""
133+
if isinstance(config, cls):
134+
return config
135+
136+
if isinstance(config, dict):
137+
return cls(**config)
138+
139+
files = files or ()
140+
return cls().merge(
141+
cls.read_from_file(filename)
142+
for use, filename in chain(
143+
((use_profile, cls.get_profile()),
144+
(use_local, cls.get_local())),
145+
zip(repeat(True), files),
146+
)
147+
if use
148+
)
149+
150+
@classmethod
151+
def get_profile(cls):
152+
return os.path.join(os.path.expanduser('~'), cls.filename)
153+
154+
@classmethod
155+
def get_local(cls):
156+
return os.path.join(os.getcwd(), cls.filename)
157+
158+
def merge(self, configs):
159+
return self._replace(**reduce(
160+
lambda a, b: (b and a.update(_coerce_dict(b))) or a,
161+
configs,
162+
self._asdict(),
163+
))
164+
165+
166+
# This lives as a class level attribute on QdbConfig.
167+
del DEFAULT_OPTIONS

qdb/tracer.py

+40-69
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from logbook import Logger, FileHandler
3434

3535
from qdb.comm import RemoteCommandManager, fmt_msg
36+
from qdb.config import QdbConfig
3637
from qdb.errors import QdbUnreachableBreakpoint, QdbQuit, QdbExecutionTimeout
3738
from qdb.utils import Timeout, default_eval_fn, default_exception_serializer
3839

@@ -55,74 +56,46 @@ def __new__(cls, *args, **kwargs):
5556
cls._instance = super(Qdb, cls).__new__(cls, *args, **kwargs)
5657
return cls._instance
5758

58-
def __init__(self,
59-
host='localhost',
60-
port=8001,
61-
auth_msg='',
62-
default_file=None,
63-
default_namespace=None,
64-
eval_fn=None,
65-
exception_serializer=None,
66-
skip_fn=None,
67-
pause_signal=None,
68-
redirect_output=True,
69-
retry_attepts=10,
70-
uuid=None,
71-
cmd_manager=None,
72-
green=False,
73-
repr_fn=None,
74-
log_file=None,
75-
execution_timeout=None):
76-
"""
77-
Host and port define the address to connect to.
78-
The auth_msg is a message that will be sent with the start event to the
79-
server. This can be used to do server/tracer authentication.
80-
The default_file is a file to use if the file field is ommited from
81-
payloads.
82-
eval_fn is the function to eval code where the user may provide it,
83-
for example in a conditional breakpoint, or in the repl.
84-
skip_fn is simmilar to the skip list feature of Bdb, except that
85-
it should be a function that takes a filename and returns True iff
86-
the debugger should skip this file. These files will be suppressed from
87-
stack traces.
88-
The pause_signal is signal to raise in this program to trigger a pause
89-
command. If this is none, this will default to SIGUSR2.
90-
retry_attempts is the number of times to attempt to connect to the
91-
server before raising a QdbFailedToConnect error.
92-
The repr_fn is a function to use to convert objects to strings to send
93-
then back to the server. By default, this wraps repr by catching
94-
exceptions and reporting them to the user.
95-
The uuid is the identifier on the server for this session. If none is
96-
provided, it will generate a uuid4.
97-
cmd_manager should be a callable that takes a Qdb instance and manages
98-
commands by implementing a next_command method. If none, a new, default
99-
manager will be created that reads commands from the server at
100-
(host, port).
101-
If green is True, this will use gevent safe timeouts, otherwise this
102-
will use signal based timeouts.
103-
repr_fn is the repr function to use when displaying results. If None,
104-
use the builtin repr.
105-
execution_timeout is the amount of time user code has to execute before
106-
being cut short. This is applied to the repl, watchlist and conditional
107-
breakpoints. If None, no timeout is applied.
59+
def __init__(self, config=None, merge=False, **kwargs):
60+
"""
61+
See qdb.config for more information about the configuration of
62+
qdb.
63+
merge denotes how config and kwargs should be merged.
64+
QdbConfig.kwargs_first says config will trample kwargs,
65+
QdbConfig.config_first says kwargs will trample config.
66+
Otherwise, kwargs and config cannot both be passed.
10867
"""
10968
super(Qdb, self).__init__()
110-
self.address = host, port
111-
self.set_default_file(default_file)
112-
self.default_namespace = default_namespace or {}
113-
self.exception_serializer = exception_serializer or \
69+
if config and kwargs:
70+
if merge == QdbConfig.kwargs_first:
71+
first = kwargs
72+
second = config
73+
elif merge == QdbConfig.config_first:
74+
first = config
75+
second = kwargs
76+
else:
77+
raise TypeError('Cannot pass config and kwargs')
78+
config = first.merge(second)
79+
else:
80+
config = QdbConfig.get_config(config or kwargs)
81+
82+
self.address = config.host, config.port
83+
self.set_default_file(config.default_file)
84+
self.default_namespace = config.default_namespace or {}
85+
self.exception_serializer = config.exception_serializer or \
11486
default_exception_serializer
115-
self.eval_fn = eval_fn or default_eval_fn
116-
self.green = green
87+
self.eval_fn = config.eval_fn or default_eval_fn
88+
self.green = config.green
11789
self._file_cache = {}
118-
self.redirect_output = redirect_output
119-
self.retry_attepts = retry_attepts
120-
self.repr_fn = repr_fn
121-
self.skip_fn = skip_fn or (lambda _: False)
122-
self.pause_signal = pause_signal if pause_signal else signal.SIGUSR2
123-
self.uuid = str(uuid or uuid4())
90+
self.redirect_output = config.redirect_output
91+
self.retry_attepts = config.retry_attepts
92+
self.repr_fn = config.repr_fn
93+
self.skip_fn = config.skip_fn or (lambda _: False)
94+
self.pause_signal = config.pause_signal \
95+
if config.pause_signal else signal.SIGUSR2
96+
self.uuid = str(config.uuid or uuid4())
12497
self.watchlist = {}
125-
self.execution_timeout = execution_timeout
98+
self.execution_timeout = config.execution_timeout
12699
# We need to be able to send stdout back to the user debugging the
127100
# program. We hold a handle to this in case the program resets stdout.
128101
if self.redirect_output:
@@ -134,13 +107,11 @@ def __init__(self,
134107
sys.stderr = self.stderr
135108
self.forget()
136109
self.log_handler = None
137-
if log_file:
138-
self.log_handler = FileHandler(log_file)
110+
if config.log_file:
111+
self.log_handler = FileHandler(config.log_file)
139112
self.log_handler.push_application()
140-
if not cmd_manager:
141-
cmd_manager = RemoteCommandManager
142-
self.cmd_manager = cmd_manager(self)
143-
self.cmd_manager.start(auth_msg)
113+
self.cmd_manager = (config.cmd_manager or RemoteCommandManager)(self)
114+
self.cmd_manager.start(config.auth_msg)
144115

145116
def clear_output_buffers(self):
146117
"""

0 commit comments

Comments
 (0)