{ "info": { "author": "Ian Fischer, Google", "author_email": "iansf@google.com", "bugtrack_url": null, "classifiers": [], "description": "# qj\n## logging designed for debugging\n### (pronounced \u2018queuedj\u2019 /kju\u02d0\u02a4/)\n\nAn easy-to-use but very expressive logging function.\n\n![qj](./qj.png)\n\nIf you have ever found yourself rewriting a list comprehension as a for loop, or\nsplitting a line of code into three lines just to store and log an intermediate\nvalue, then this log function is for you.\n\n\n## Overview:\n\nqj is meant to help debug python quickly with lightweight log messages that are\neasy to add and remove.\n\nOn top of the basic logging functionality, qj also provides a collection of\ndebugging helpers that are simple to use, including the ability to drop into\nthe python debugger in the middle of a line of code.\n\n\n## Examples:\n\n### Instead of turning this:\n```\nresult = some_function(another_function(my_value))\n```\n#### into this:\n```\ntmp = another_function(my_value)\nlogging.info('tmp = %r', tmp)\nresult = some_function(tmp)\n```\n#### you can just do this:\n```\nresult = some_function(qj(another_function(my_value)))\n```\n\n### Instead of turning this:\n```\nmy_list = [process_value(value)\n for value in some_function(various, other, args)\n if some_condition(value)]\n```\n#### into this:\n```\nmy_list = []\nfor value in some_function(various, other, args):\n logging.info('value = %r', value)\n condition = some_condition(value)\n logging.info('condition = %r', condition)\n if condition:\n final_value = process_value(value)\n logging.info('final_value = %r', final_value)\n my_list.append(final_value)\nlogging.info('my_list = %r', my_list)\n```\n#### you can keep it as the list comprehension:\n```\nmy_list = qj([qj(process_value(qj(value)))\n for value in some_function(various, other, args)\n if qj(some_condition(value))])\n```\n\n## Philosophy:\n\nThere are two reasons we add logs to our code:\n 1. We want to communicate something to the user of the code.\n 2. **We want to debug.**\n\nIn python, as well as most other languages, the default logging mechanisms\nserve the first purpose well, but make things unnecessarily difficult for the\nsecond purpose.\n\n### Debug logging should have no friction.\n\nWhen you have of a question about your code, you should not be tempted to just\nthink hard to try to come up with the answer. Instead, you should know that\nyou can type just a few characters to see the answer.\n\n### You should never have to rewrite code just to check it for bugs.\n\nThe most important feature of a debug logger is that it always returns its\nargument. This allows you to add logging calls pretty much anywhere in your\ncode without having to rewrite your code or create temporary variables.\n\nThis is a minimal implementation of qj:\n```\ndef qj(x):\n print(x)\n return x\n```\n\nOnce you have that core property, there are a lot of other useful things you find\nyourself wanting that make debugging easier. qj attempts to cleanly pull\ndebugging-related features together into a very simple power-user interface.\nTo that end, most argument names are single letters that are mnemonics for the\nparticular feature:\n - `x` is for the input that you want to log and return.\n - `s` is for the string you want to describe `x`.\n - `d` is for the debugger.\n - `b` is for the boolean that turns everything off.\n - `l` is for the lambda that lets you log more things.\n - `p` is for printing public properties of `x`.\n - `n` is for numpy array statistics.\n - `t` is for printing tensorflow Tensors.\n - `r` is for overriding the return value.\n - `z` is for zeroing out the log count and automatic indentation of a particular log.\n\nA few less-commonly needed features get longer names:\n - `pad` is for padding a log message so that it stands out.\n - `tfc` is for checking numerics on tensorflow Tensors.\n - `tic` and `toc` are for measuring and logging timing of arbitrary chunks of code.\n - `time` is for measuring and logging timing stats for a callable.\n - `catch` is for catching exceptions from a callable.\n - `log_all_calls` is for wrapping `x` such that all public method calls and\n their return values get logged.\n\n### The right description of `x` is usually its source code.\n\nIf you want to log the value of a variable named `long_variable_name`, you shouldn't\nneed to think about how to describe the variable in the log message so you can find it.\nIts name and the line number where you are logging it are its best description.\n`qj(long_variable_name)` logs something like this:\n```\nqj: some_func: long_variable_name : \n```\n\nSimilarly, logging the value of a complicated expression should use the expression\nitself as the description. `qj(foo * 2 + bar ** 2)` logs something like:\n```\nqj: some_func: foo * 2 + bar ** 2 : 42\n```\n\n### You shouldn't need to `import` just to log debug messages.\n\nIdeally, something like qj would be available as a builtin in python. We can get pretty\nclose to that ideal by providing a way to install qj into the global namespace after\nimporting it the first time. This means that you can pretend qj is a builtin and use it\nin any python code that runs after you import it once, even if the original import is in\nsome other file, package, or library.\n\n### Adding logs should be easy. So should removing logs.\n\nThe name qj is meant to be easy to type (two characters in opposite hands) and\neasy to search for and highlight in your code. 'qj' is one of the least\nfrequently occurring bigrams based on a survey of millions of lines of python\ncode, so it is hopefully very unlikely to occur naturally in your code. This\nproperty will help you find and remove all of your debug logs easily once you\nhave fixed your bugs.\n\n### Logs should be easy to read.\n\nqj defaults to using colors. The metadata and description of the log are in red.\nThe value of the log is in green. Your debug logs will stand out strongly against\nwhatever normal logging your code does.\n\n![qj](./qj.png)\n\nqj also works to align log messages nicely, where possible, to help you visually\ngroup related log messages together.\n\n\n## Basic Usage:\n\n### Install with pip:\n```\n$ pip install qj\n```\n\n### Add the following import:\n```\nfrom qj_global import qj\n```\nThis makes qj globally available in any python code that is run after the\nimport. It's often nice to import qj from your main script once, since you\ncan then use it in your entire project (and even in other python libraries).\nSee [Global Access](#global-access) for more information on importing.\n\n### If your problem code looks like this:\n```\ndef problem_code(...):\n ...\n problem_array = [other_func(value, other_args)\n for value in compute_some_array(more_args)\n if some_tricky_condition]\n```\n### Make it look like this:\n```\ndef problem_code(...):\n ...\n problem_array = qj([qj(other_func(qj(value), qj(other_args)))\n for value in qj(compute_some_array(qj(more_args)))\n if qj(some_tricky_condition)])\n```\n\nIn most cases, you shouldn't need to put logs on everything like that, of\ncourse. If your debug cycle is fast, you can add the logs more selectively to\navoid getting overwhelmed by new logspam.\n\nThese changes will result in detailed logs that tell you what function they\nare running in, what line number they are on, what source code for the log is,\nand what its value is.\n\nThe log messages will also be indented some amount that corresponds to how\nmany calls to qj are in the current code context. This is particularly\nuseful with comprehensions, since python reports the last line of the\ncomprehension in logs and stack traces, which is often not the correct line\nwhen dealing with long comprehensions (or even long argument lists).\n\n### This is the general layout of the basic log message:\n```\n[datetime] qj: function: [indentation] source code or description : value\n```\nIn the example above, the log messages might look like:\n```\nqj: problem_code: more_args <92>: ['list', 'of', 'more_args']\nqj: problem_code: compute_some_array(qj(more_args)) <92>: ['list', 'of', 'things', 'hey!']\nqj: problem_code: some_tricky_condition <92>: True\nqj: problem_code: other_args <92>: ['list', 'of', 'other_args']\nqj: problem_code: value <92>: list\nqj: problem_code: other_func(qj(value), qj(other_args)) <92>: other_func_return list\nqj: problem_code: some_tricky_condition <92>: True\nqj: problem_code: other_args <92>: ['list', 'of', 'other_args']\nqj: problem_code: value <92>: of\nqj: problem_code: other_func(qj(value), qj(other_args)) <92>: other_func_return of\nqj: problem_code: some_tricky_condition <92>: False\nqj: problem_code: some_tricky_condition <92>: True\nqj: problem_code: other_args <92>: ['list', 'of', 'other_args']\nqj: problem_code: value <92>: hey!\nqj: problem_code: other_func(qj(value), qj(other_args)) <92>: other_func_return hey!\nqj: problem_code: [qj(other_func(qj(value), qj(other_args))) ...] <92>: ['other_func_return list', 'other_func_return of', 'other_func_return hey!']\n```\n\nThings to note in that output:\n - The indentation automatically gives a visual indicator of how the\n comprehension is being computed -- you can see how the loops happen and\n when an iteration gets skipped at a glance or so (e.g., the two lines with\n the same indention should jump out, and closer inspection shows that the\n if statement generated the False, which explains why the\n previous indentation pattern didn't repeat).\n - You didn't have to specify any logging strings -- qj extracted the source code from the call site.\n\n### You can change the description string with `qj(foo, 'this particular foo')`:\n```\nqj: some_func: this particular foo <149>: foo\n```\n\nIf qj can't find the correct source code, it will log the type of the output instead.\nIf that happens, or if you don't want to see the line of code, you might change the\nprevious logging to look like this:\n```\ndef problem_code(...):\n ...\n problem_array = qj([qj(other_func(qj(value), qj(other_args)), 'other_func return')\n for value in qj(s='computed array', x=compute_some_array(qj(more_args)))\n if qj(s='if', x=some_tricky_condition)], 'problem_array')\n\nqj: problem_code: more_args <153>: ['list', 'of', 'more_args']\nqj: problem_code: computed array <153>: ['list', 'of', 'things', 'hey!']\nqj: problem_code: if <153>: True\nqj: problem_code: other_args <153>: ['list', 'of', 'other_args']\nqj: problem_code: value <153>: list\nqj: problem_code: other_func return <153>: other_func_return list\nqj: problem_code: if <153>: True\nqj: problem_code: other_args <153>: ['list', 'of', 'other_args']\nqj: problem_code: value <153>: of\nqj: problem_code: other_func return <153>: other_func_return of\nqj: problem_code: if <153>: False\nqj: problem_code: if <153>: True\nqj: problem_code: other_args <153>: ['list', 'of', 'other_args']\nqj: problem_code: value <153>: hey!\nqj: problem_code: other_func return <153>: other_func_return hey!\nqj: problem_code: problem_array <153>: ['other_func_return list', 'other_func_return of', 'other_func_return hey!']\n```\nNote that both positional arguments `qj(value, 'val')` and keyword arguments `qj(s='val', x=value)` can be used.\n\n\n## Advanced Usage:\nThese are ordered by the likelihood that you will want to use them.\n\n### You can enter the debugger with `qj(d=1)`:\nThis drops you into the debugger -- it even works in jupyter notebooks!\n\nYou can use this to drop into the debugger in the middle of executing a comprehension:\n```\n[qj(d=(value=='foo'), x=value) for value in ['foo', 'bar']]\n\nqj: some_func: d=(value=='foo'), x=value <198>: foo\n> (198)some_func()\n----> 198 [qj(d=(value=='foo'), x=value) for value in ['foo', 'bar']]\n\nipdb> value\n'foo'\n```\n\n\n### You can selectively turn logging off with `qj(foo, b=0)`:\nThis can be useful if you only care about logging when a particular value shows up:\n```\n[qj(b=('f' in value), x=value) for value in ['foo', 'bar']]\n\nqj: some_func: b=('f' in value), x=value <208>: foo\n```\nNote the lack of a log for 'bar'.\n\n\n### If logging is disabled for any reason, the other argument-based features will not trigger either:\n```\nqj(foo, d=1, b=(foo == 'foo'))\n```\nThis will only drop into the debugger if `foo == 'foo'`.\n\nLogging can be disabled for three reasons:\n 1. `b=False`, as described above.\n 2. `qj.LOG = False` (see [Parameters](#parameters) below).\n 3. You are attempting to print more than `qj.MAX_FRAME_LOGS` in the current\n stack frame (see [Parameters](#parameters) below).\n\n\n### You can log extra context with `qj(foo, l=lambda _: other_vars)`:\nThis is useful for logging other variables in the same context:\n```\n[qj(foo, l=lambda _: other_comp_var) for foo, other_comp_var in ...]\n\nqj: some_func: foo, l=lambda _: other_comp_var <328>: foo\nqj: some_func: ['other', 'comprehension', 'var']\nqj: some_func: foo, l=lambda _: other_comp_var <328>: bar\nqj: some_func: ['other', 'comprehension', 'var']\n```\n\nThe input (`x`) is passed as the argument to the lambda:\n```\nqj(foo, l=lambda x: x.replace('f', 'z'))\n\nqj: some_func: foo, l=lambda x: x.replace('f', 'z') <336>: foo\nqj: some_func: zoo\n\n```\nNote that qj attempts to nicely align the starts of log messages that are all generated from the same call to qj.\n\n\n### You can log the timing of arbitrary code chunks with `qj(tic=1) ... qj(toc=1)`:\n```\nqj(tic=1)\ndo_a_bunch()\nof_things()\nqj(toc=1)\n\nqj: some_func: tic=1 <347>: Adding tic.\nqj: some_func: toc=1 <350>: Computing toc.\nqj: 2.3101 seconds since tic=1.\n```\n\nYou can nest `tic` and `toc`:\n```\nqj(tic=1)\ndo_something()\nqj(tic=2)\ndo_something_else()\nqj(toc=1)\nfinish_up()\nqj(toc=1)\n\nqj: some_func: tic=1 <348>: Adding tic.\nqj: some_func: tic=2 <350>: Adding tic.\nqj: some_func: toc=1 <352>: Computing toc.\nqj: 0.5200 seconds since tic=2.\nqj: some_func: toc=1 <354>: Computing toc.\nqj: 1.3830 seconds since tic=1.\n```\n\nSince any `True` value will turn on `tic`, you can use it as a convenient identifier, as above where\n`tic=2` is the second tic. The actual identifier printed by `toc` is whatever description string was\nused for the `tic`, though, so you can give descriptive names just as in any other log message:\n```\nqj(foo, 'start foo', tic=1)\nfoo.start()\nqj(foo.finish(), toc=1)\n\nqj: some_func: start foo <367>: \nqj: Added tic.\nqj: some_func: foo.finish(), toc=1 <369>: Foo.SUCCESSFUL_FINISH\nqj: 5.9294 seconds since start foo.\n```\n\nYou can use `tic` and `toc` in the same call to log the duration of any looping code:\n```\n[qj(x, tic=1, toc=1) for x in [1, 2, 3]]\n\nqj: some_func: x, tic=1, toc=1 <380>: 1\nqj: Added tic.\nqj: some_func: x, tic=1, toc=1 <380>: 2\nqj: 0.0028 seconds since x, tic=1, toc=1.\nqj: Added tic.\nqj: some_func: x, tic=1, toc=1 <380>: 3\nqj: 0.0028 seconds since x, tic=1, toc=1.\nqj: Added tic.\n```\n\nYou can use `toc=-1` to clear out all previous `tic`s:\n```\nqj(tic=1)\ndo_something()\nqj(tic=2)\ndo_something_else()\nqj(tic=3)\nfinish_up()\nqj(toc=-1)\n\nqj: some_func: tic=1 <394>: Adding tic.\nqj: some_func: tic=2 <396>: Adding tic.\nqj: some_func: tic=3 <398>: Adding tic.\nqj: some_func: toc=1 <400>: Computing toc.\nqj: 0.2185 seconds since tic=3.\nqj: 0.5200 seconds since tic=2.\nqj: 1.3830 seconds since tic=1.\n```\n\n\n### You can log the public properties for the input with `qj(foo, p=1)`:\n```\nqj(some_object, p=1)\n\nqj: some_func: some_object, p=1: some_object.__str__() output\nqj: Public properties:\n some_method(a, b=None, c='default')\n some_public_property\n```\nNote that this can be dangerous. In order to log the method signatures,\nPython's `inspect` module can actually cause code to execute on your object.\nSpecifically, if you have `@property` getters on your object, that code will\nbe run. If your `@property` getter changes state, using this flag to print the\nobject's public API will change your object's state, which might make your\ndebugging job even harder. (Of course, you should never change state in a getter.)\n\nThis is generally useful to quickly check the API of an unfamiliar object\nwhile working in a jupyter notebook.\n\n\n### You can log some useful stats about x instead of its value with `qj(arr, n=1)`:\n```\nqj: some_func: arr, n=1 (shape (min (mean std) max) hist) <257>: ((100, 1), (0.00085, (0.46952, 0.2795), 0.97596), array([25, 14, 23, 23, 15]))\n```\nThis only works if the input (`x`) is a numeric numpy array or can be cast to one,\nand if numpy has already been imported somewhere in your code. Otherwise, the value\nof `x` is logged as normal.\n\nThe log string is augmented with a key to the different parts of the logged value.\n\nThe final value is a histogram of the array values. The number of histogram buckets\ndefaults to 5, but can be increased by passing an integer to `n` greater than 5:\n```\nqj(arr, n=10)\n\nqj: some_func: arr, n=10 ...: (..., array([11, 14, 8, 6, 10, 13, 14, 9, 7, 8]))\n```\n\n\n### You can add a `tensorflow.Print` call to `x` with `y = qj(some_tensor, t=1)`:\n```\nqj: some_func: some_tensor, t=1 <258>: Tensor(\"some_tensor:0\", ...)\nqj: Wrapping return value in tf.Print operation.\n```\nAnd then later:\n```\nsess.run(y)\n\nqj: some_func: some_tensor, t=1 <258>: [10 1] [[0.64550841]...\n```\nNote that the Tensorflow output includes the shape of the tensor first (`[10 1]`),\nfollowed by its value. This only works if x is a `tf.Tensor` object and\n`tensorflow` has already been imported somewhere in your code.\n\nThe number of logs printed from the `tf.Print` call is `qj.MAX_FRAME_LOGS`\n(described [below](parameters), defaults to 200) if `t is True`. Otherwise,\nit is set to `int(t)`. Thus, `t=1` prints once, but `t=True` prints 200 times.\n\n\n### You can also turn on numerics checking for any `tf.Tensor` with `y = qj(some_tensor, tfc=1)`:\nFor example, `log(0)` is not a number:\n```\ny = qj(tf.log(tensor_with_zeros), tfc=1)\n\nqj: some_func: tf.log(tensor_with_zeros), tfc=1 <258>: Tensor(\"Log:0\", ...)\nqj: Wrapping return value in tf.check_numerics.\n```\nAnd then later:\n```\nsess.run(y)\n\nInvalidArgumentError: qj: some_func: tf.log(tensor_with_zeros), tfc=1 <258> : Tensor had Inf values\n```\nNote that tf.check_numerics is very slow, so you won't want to leave these in your graph.\nThis also only works if x is a `tf.Tensor` object and `tensorflow` has already been\nimported somewhere in your code.\n\n### You can override the return value of qj by passing any value to `r`:\n```\nsome_function(normal_arg, special_flag=qj(some_value, r=None))\n\nqj: some_func: some_value, r=None <272>: some flag value\nqj: Overridden return value: None\n```\nAs in the example, this can be useful to temporarily change or turn off a\nvalue being passed to a function, rather than having to delete the value,\nwhich you might forget about.\n\n\n### You can add timing logs to any function with `@qj(time=1)` or `qj(foo, time=100)`:\n```\n@qj(time=1)\ndef foo():\n ...\n\nqj: module_level_code: time=1 <343>: Preparing decorator to measure timing...\nqj: Decorating with timing function.\n\nfoo()\n\nqj: some_func: Average timing for across 1 call <343>: 0.0021 seconds\n```\nNote that the log message is reported from the location of the call to the function that generated the message\n(in this case, line 343 in `some_file.py`, inside of `some_func`).\n\nSetting `time` to a larger integer value will report timing stats less freqently:\n```\nfoo = qj(foo, time=1000)\nfor _ in range(1000):\n foo()\n\nqj: some_func: foo, time=1000 <359>: \nqj: Wrapping return value in timing function.\nqj: some_func: Average timing for across 1000 calls <361>: 0.0023 seconds\n```\n\n\n### You can catch exceptions and drop into the debugger with `@qj(catch=1)` or `qj(foo, catch=)`:\n```\n@qj(catch=1)\ndef foo(): raise Exception('FOO!')\n\nqj: module_level_code: catch=1 <371>: Preparing decorator to catch exceptions...\nqj: Decorating with exception function.\n\nfoo()\n\nqj: some_func: Caught an exception in <377>: FOO!\n> (377)()\n----> 1 foo()\n\nipdb>\n```\n\nThis can be particularly useful in comprehensions where you want to be able to inspect\nthe state of the comprehension that led to an exception:\n```\n[qj(foo, catch=1)(x) for x in [1, 2, 3]]\n\nqj: some_func: foo, catch=1 <389>: \nqj: Wrapping return value in exception function.\nqj: some_func: Caught an exception in <389>: FOO!\n...\n> (389)()\n----> 1 [qj(foo, catch=1)(x) for x in [1, 2, 3]]\n\nipdb> x\n1\n```\nSetting `catch` will always drop into the debugger when an exception is caught -- this feature\nis for debugging exceptions, not for replacing appropriate use of `try: ... except:`.\n\n\n### You can log all future calls to an object with `qj(foo, log_all_calls=1)`:\n```\ns = qj('abc', log_all_calls=1)\n\nqj: some_func: 'abc', log_all_calls=1 <380>: abc\nqj: Wrapping all public method calls for object.\n\ns.replace('a', 'b')\n\nqj: some_func: calling replace <385>: replace('a', 'b')\nqj: some_func: returning from replace <385>: bbc\n```\n\nThis can break your code in a variety of ways and fail silently in other ways, but some problems\nare much easier to debug with this functionality. For example, figuring out why sequences of numbers\nfrom a seeded random number generator differ on different runs with the same seed:\n```\nrng = qj(np.random.RandomState(1), log_all_calls=1)\n\nqj: some_func: np.random.RandomState(1), log_all_calls=1 <395>: \nqj: Wrapping all function calls for object.\n\nfor k in set(list('abcdefghijklmnop')):\n rng.randint(ord(k))\n\n# First run:\nqj: some_func: calling randint <413>: randint(97)\nqj: some_func: returning from randint <413>: 37\nqj: some_func: calling randint <413>: randint(99)\nqj: some_func: returning from randint <413>: 12\nqj: some_func: calling randint <413>: randint(98)\nqj: some_func: returning from randint <413>: 72\n...\n\n\n# Subsequent run with a reseeded rng:\nqj: some_func: calling randint <413>: randint(101)\nqj: some_func: returning from randint <413>: 9\nqj: some_func: calling randint <413>: randint(100)\nqj: some_func: returning from randint <413>: 75\nqj: some_func: calling randint <413>: randint(103)\nqj: some_func: returning from randint <413>: 5\n...\n```\nThis fails (in theory, but not as written) because sets are iterated in an undefined order.\nSimilar failures are possible with much more subtle structure. Comparing different series\nof log calls makes it very easy to find exactly where the series diverge, which gives a good\nchance of figuring out what the bug is.\n\n\n### You can make particular log messages stand out with `qj(foo, pad=)`:\n```\nqj(foo, pad='#')\n\n##################################################\nqj: some_func: foo, pad='#' <461>: foo\n##################################################\n```\n\nSimilarly, add blank lines:\n```\nqj(foo, pad=3)\n\n# Some other log message...\n\n\n\nqj: some_func: foo, pad=3 <470>: foo\n\n\n\n# The next log message...\n```\n\n## Parameters:\n\n### There are seven global parameters for controlling the logger:\n 1. `qj.LOG`: Turns logging on or off globally. Starts out set to True, so\n logging is on.\n 2. `qj.LOG_FN`: Which log function to use. All log messages are passed to this\n function as a fully-formed string, so the only constraints are\n that this function takes a single parameter, and that it _is_\n a function -- e.g., you can't set `qj.LOG_FN = print` unless\n you are using `from __future__ import print_function` (although\n you can define your own log function that just calls print if\n you don't like the default). Defaults to `logging.info` wrapped\n in a lambda to support colorful logs.\n 3. `qj.STR_FN`: Which string conversion function to use. All objects to be logged\n are passed to this function directly, so it must take an arbitrary\n python object and return a python string. Defaults to `str`, but a\n nice override is `pprint.pformat`.\n 4. `qj.MAX_FRAME_LOGS`: Limits the number of times per stack frame the logger\n will print for each qj call. If the limit is hit, it\n prints an informative message after the last log of the\n frame. Defaults to 200.\n 5. `qj.COLOR`: Turns colored log output on or off globally. Starts out set to\n True, so colorized logging is on.\n 6. `qj.PREFIX`: String that all qj logs will use as a prefix. Defaults to `'qj: '`.\n 7. `qj.DEBUG_FN`: Which debugger to use. You shouldn't need to set this in most\n situations. The function needs to take a single argument, which\n is the stack frame that the logger should start in. If this is\n not set, then the first time debugging is requested, qj attempts\n to load ipdb. If ipdb isn't available, it falls back to using pdb.\n In both cases, `qj.DEBUG_FN` is set to the respective `set_trace`\n function in a manner that supports setting the stack frame.\n\n\n## Global Access:\nIn many cases when debugging, you need to dive into many different files\nacross many different modules. In those cases, it is nice to have a single\nlogging and debugging interface that is immediately available in all of the\nfiles you touch, without having to import anything additional in each file.\n\nTo support this use case, you can call the following function after importing\nin one file:\n```\nfrom qj import qj\nqj.make_global()\n```\nThis will add qj to python's equivalent of a global namespace, allowing you\nto call qj from any python code that runs after the call to\n`qj.make_global()`, no matter what file or module it is in.\n\nWhen using qj from a jupyter notebook, qj.make_global() is automatically called\nwhen qj is imported.\n\nAs described in [Basic Usage](basic-usage), you can also just use:\n```\nfrom qj_global import qj\n```\nThis is generally what you want, but qj does not force you to pollute the global\nnamespace if you don't want to (except in jupyter notebooks).\n\n\n## qj Magic Warning:\n\nqj adds a local variable to the stack frame it is called from. That variable is\n`__qj_magic_wocha_doin__`. If you happen to have a local variable with the same\nname and you call qj, you're going to have a bad time.\n\n\n## Testing:\n\nqj has extensive tests. You can run them with nosetests:\n```\n$ nosetests\n........................................................................................\n----------------------------------------------------------------------\nRan 88 tests in 1.341s\n\nOK\n```\n\nOr you can run them directly:\n```\n$ python qj/tests/qj_test.py\n........................................................................................\n----------------------------------------------------------------------\nRan 88 tests in 1.037s\n\nOK\n```\n\nIf you have both python 2.7 and python 3.6+ installed, you can test both versions:\n```\n$ nosetests --where=qj/tests --py3where=qj/tests --py3where=/qjtests3\n$ python3 qj/tests/qj_test.py\n$ python3 qj/tests3/qj_test.py\n```\n\n## Disclaimer:\n\nThis project is not an official Google project. It is not supported by Google\nand Google specifically disclaims all warranties as to its quality,\nmerchantability, or fitness for a particular purpose.\n\n\n## Contributing:\n\nSee how to [contribute](./CONTRIBUTING.md).\n\n\n## License:\n\n[Apache 2.0](./LICENSE).\n\n", "description_content_type": "text/markdown", "docs_url": null, "download_url": "https://github.com/iansf/qj/archive/0.1.6.tar.gz", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/iansf/qj", "keywords": "", "license": "Apache 2.0", "maintainer": "", "maintainer_email": "", "name": "qj", "package_url": "https://pypi.org/project/qj/", "platform": "", "project_url": "https://pypi.org/project/qj/", "project_urls": { "Download": "https://github.com/iansf/qj/archive/0.1.6.tar.gz", "Homepage": "https://github.com/iansf/qj" }, "release_url": "https://pypi.org/project/qj/0.1.6/", "requires_dist": null, "requires_python": "", "summary": "qj: logging designed for debugging.", "version": "0.1.6" }, "last_serial": 5496036, "releases": { "0.1.0": [ { "comment_text": "", "digests": { "md5": "80662b90f393a25d86bbb567ed186406", "sha256": "e0920954e0cceb56888b7346e4726d7f2bb9bf7630ce844b6b4cf9feec5cd84b" }, "downloads": -1, "filename": "qj-0.1.0.tar.gz", "has_sig": false, "md5_digest": "80662b90f393a25d86bbb567ed186406", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 48611, "upload_time": "2017-12-03T02:48:35", "url": "https://files.pythonhosted.org/packages/46/13/dc07111c1ef5fc712c8874ce60efb9a0f33f43b42752a904ea3f78a6f093/qj-0.1.0.tar.gz" } ], "0.1.1": [ { "comment_text": "", "digests": { "md5": "ff151ea323d56e1f021b34830c592b8d", "sha256": "cf3187a56de5616332b7834248f36b50ae6a70f87ce1112d0a9082300faa2e48" }, "downloads": -1, "filename": "qj-0.1.1.tar.gz", "has_sig": false, "md5_digest": "ff151ea323d56e1f021b34830c592b8d", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 47613, "upload_time": "2018-02-20T16:22:08", "url": "https://files.pythonhosted.org/packages/13/42/d535d64af9054c09c68258603fe0ef635d03a58384ecb43befa945fa967e/qj-0.1.1.tar.gz" } ], "0.1.2": [ { "comment_text": "", "digests": { "md5": "9c0402dbc727bb139e85ad2f0b87787b", "sha256": "0d7dc054f7cc0ec4ec1b6e39244e806e6366a05c4c2d86fa2db5087a24403a9b" }, "downloads": -1, "filename": "qj-0.1.2.tar.gz", "has_sig": false, "md5_digest": "9c0402dbc727bb139e85ad2f0b87787b", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 47757, "upload_time": "2018-03-05T19:21:38", "url": "https://files.pythonhosted.org/packages/a8/a0/44bf2d0c628057620841cc11e2519aa778fd81f20bfd0c6b36fb060df9e3/qj-0.1.2.tar.gz" } ], "0.1.3": [ { "comment_text": "", "digests": { "md5": "6ce5623cf9c47bb2a4eaba515ce73211", "sha256": "5b9ee0ebd52c9e2415a2d5486a6f1c7a95e526766edc590924a9a11fa0f2f0db" }, "downloads": -1, "filename": "qj-0.1.3.tar.gz", "has_sig": false, "md5_digest": "6ce5623cf9c47bb2a4eaba515ce73211", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 48024, "upload_time": "2018-03-24T20:48:35", "url": "https://files.pythonhosted.org/packages/b2/76/63e9c9da77ee314ca4f36dd23b2d15a8fa653bd2f8f65ad9c9cc2ebc9efa/qj-0.1.3.tar.gz" } ], "0.1.4": [ { "comment_text": "", "digests": { "md5": "bab1f629d132c282b3540206a8f8ccae", "sha256": "e25729ef69e147ac5e16ac4ffc0662dc4abc852e8b0ac6f6dcc6e665ba89685d" }, "downloads": -1, "filename": "qj-0.1.4.tar.gz", "has_sig": false, "md5_digest": "bab1f629d132c282b3540206a8f8ccae", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 48032, "upload_time": "2018-03-24T21:11:16", "url": "https://files.pythonhosted.org/packages/dc/cf/27b29569f09255f0b52c75dd4650677eb818cf1318890fa40a877edcc545/qj-0.1.4.tar.gz" } ], "0.1.5": [ { "comment_text": "", "digests": { "md5": "523fcbc7767e91028a06ab2bf397d3e6", "sha256": "8036d4857084e22bc14bac45ca43ab30d576bcba3ecdfe0e6640ec5f73604de8" }, "downloads": -1, "filename": "qj-0.1.5.tar.gz", "has_sig": false, "md5_digest": "523fcbc7767e91028a06ab2bf397d3e6", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 48078, "upload_time": "2019-01-14T19:30:00", "url": "https://files.pythonhosted.org/packages/f6/8b/4a27290d5b21bfdfcb52c7f4c10e4cd3c51b40aa171fcd987d25bf85ac51/qj-0.1.5.tar.gz" } ], "0.1.6": [ { "comment_text": "", "digests": { "md5": "36f93dc21e210e17fff1dca3553721f4", "sha256": "6ac42661b5dc63cf13371a2dff202a190e9dc3b77d96be94c408a527cf1b9a38" }, "downloads": -1, "filename": "qj-0.1.6-py3-none-any.whl", "has_sig": false, "md5_digest": "36f93dc21e210e17fff1dca3553721f4", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 32127, "upload_time": "2019-07-07T02:01:29", "url": "https://files.pythonhosted.org/packages/3f/1e/797f28bdf3c550686d5ef38fac5287b044674dd94a66b21ba990fb40429d/qj-0.1.6-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "a778e18411842fbcfe8b1c9935871516", "sha256": "82f0eac64b82fdfbe6adfa2ea6036951011a42ead38c7c902f45194517c70626" }, "downloads": -1, "filename": "qj-0.1.6.tar.gz", "has_sig": false, "md5_digest": "a778e18411842fbcfe8b1c9935871516", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 48478, "upload_time": "2019-07-07T02:01:30", "url": "https://files.pythonhosted.org/packages/af/71/d9c75276051e009a4544f1ed463dc423eb122ad044a09325449d17116210/qj-0.1.6.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "36f93dc21e210e17fff1dca3553721f4", "sha256": "6ac42661b5dc63cf13371a2dff202a190e9dc3b77d96be94c408a527cf1b9a38" }, "downloads": -1, "filename": "qj-0.1.6-py3-none-any.whl", "has_sig": false, "md5_digest": "36f93dc21e210e17fff1dca3553721f4", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 32127, "upload_time": "2019-07-07T02:01:29", "url": "https://files.pythonhosted.org/packages/3f/1e/797f28bdf3c550686d5ef38fac5287b044674dd94a66b21ba990fb40429d/qj-0.1.6-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "a778e18411842fbcfe8b1c9935871516", "sha256": "82f0eac64b82fdfbe6adfa2ea6036951011a42ead38c7c902f45194517c70626" }, "downloads": -1, "filename": "qj-0.1.6.tar.gz", "has_sig": false, "md5_digest": "a778e18411842fbcfe8b1c9935871516", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 48478, "upload_time": "2019-07-07T02:01:30", "url": "https://files.pythonhosted.org/packages/af/71/d9c75276051e009a4544f1ed463dc423eb122ad044a09325449d17116210/qj-0.1.6.tar.gz" } ] }