ycecream
Project description
Do you ever use print() or log() to debug your code? If so, ycecream, or y for short, will make printing debug information a lot sweeter. And on top of that, you get some basic benchmarking functionality.
Installation
Installing ycecream with pip is easy.
$ pip install ycecream
or when you want to upgrade,
$ pip install ycecream --upgrade
Alternatively, ycecream.py can be juist copied into you current work directory from GitHub (https://github.com/salabim/ycecream).
No dependencies!
Inspect variables and expressions
Have you ever printed variables or expressions to debug your program? If you’ve ever typed something like
print(add2(1000))
or the more thorough
print("add2(1000)", add2(1000)))
or (for Python >= 3.8 only):
print(f"{add2(1000) =}")
then y() is here to help. With arguments, y() inspects itself and prints both its own arguments and the values of those arguments.
from ycecream import y def add2(i): return i + 2 y(add2(1000))
prints
y| add2(1000): 1002
Similarly,
from ycecream import y class X: a = 3 world = {"EN": "world", "NL": "wereld", "FR": "monde", "DE": "Welt"} y(world, X.a)
prints
y| world: {"EN": "world", "NL": "wereld", "FR": "monde", "DE": "Welt"}, X.a: 3
Just give y() a variable or expression and you’re done. Sweet, isn’t it?
Inspect execution
Have you ever used print() to determine which parts of your program are executed, and in which order they’re executed? For example, if you’ve ever added print statements to debug code like
def add2(i): print("enter") result = i + 2 print("exit") return result
then y() helps here, too. Without arguments, y() inspects itself and prints the calling line number and -if applicable- the file name and parent function.
from ycecream import y def add2(i): y() result = i + 2 y() return result y(add2(1000))
prints something like
y| #3 in add2() y| #5 in add2() y| add2(1000): 1002
Just call y() and you’re done. Isn’t that sweet?
Return Value
y() returns its argument(s), so y() can easily be inserted into pre-existing code.
from ycecream import y def add2(i): return i + 2 b = y(add2(1000)) y(b)
prints
y| add2(1000): 1002 y| b: 1002
Debug entry and exit of function calls
When you apply y() as a decorator to a function or method, both the entry and exit can be tracked. The (keyword) arguments passed will be shown and upon return, the return value.
from ycecream import y @y() def mul(x, y): return x * y print(mul(5, 7))
prints
y| called mul(5, 7) y| returned 35 from mul(5, 7) in 0.000006 seconds 35
It is possible to suppress the print-out of either the enter or the exit information with the show_enter and show_exit parameters, like:
inport ycecream as y @y(show_exit=False) def mul(x, y): return x * y print(mul(5, 7))
prints
y| called mul(5, 7) 35
Note that it is possible to use y as a decorator without the parentheses, like
@y def diode(x): return 0 if x<0 else x
, but this might not work correctly when the def/class definition spawns more than one line. So, always use y() or y(<parameters>) when used as a decorator.
Benchmarking with ycecream
If you decorate a function or method with y, you will be offered the duration between entry and exit (in seconds) as a bonus.
That opens the door to simple benchmarking, like:
from ycecream import y import time @y(show_enter=False,show_line_number=True) def do_sort(i): n = 10 ** i x = sorted(list(range(n))) return f"{n:9d}" for i in range(8): do_sort(i)
the ouput will show the effects of the population size on the sort speed:
y| #5 ==> returned ' 1' from do_sort(0) in 0.000027 seconds y| #5 ==> returned ' 10' from do_sort(1) in 0.000060 seconds y| #5 ==> returned ' 100' from do_sort(2) in 0.000748 seconds y| #5 ==> returned ' 1000' from do_sort(3) in 0.001897 seconds y| #5 ==> returned ' 10000' from do_sort(4) in 0.002231 seconds y| #5 ==> returned ' 100000' from do_sort(5) in 0.024014 seconds y| #5 ==> returned ' 1000000' from do_sort(6) in 0.257504 seconds y| #5 ==> returned ' 10000000' from do_sort(7) in 1.553495 seconds
It is also possible to time any code by using y as a context manager, e.g.
with y(): time.sleep(1)
wil print something like
y| enter y| exit in 1.000900 seconds
You can include parameters here as well:
with y(show_context=True, show_time=True): time.sleep(1)
will print somethink like:
y| #8 @ 13:20:32.605903 ==> enter y| #8 @ 13:20:33.609519 ==> exit in 1.003358 seconds
Finally, to help with timing code, you can request the current delta with
y().delta
or (re)set it with
y().delta = 0
So, e.g. to time a section of code:
y.delta = 0 time.sleep(1) duration = y.delta y(duration)
might print:
y| duration: 1.0001721999999997
Configuration
For the configuration, it is important to realize that y is an instance of the ycecream._Y class, which has a number of configuration attributes:
-------------------------------------------------- attribute alternative default -------------------------------------------------- prefix p "y| " output o "stderr" serialize pprint.pformat show_line_number sln False show_time st False show_delta sd False show_enter se True show_exit sx True sort_dicts *) sdi False enabled e True line_length ll 80 compact *) c False indent i 1 depth de 1000000 wrap_indent wi " " context_delimiter cd " ==> " pair_delimiter pd ", " values_only vo False return_none rn False enforce_line_length ell False decorator d False context_manager cm False -------------------------------------------------- *) ignored under Python 2.7
It is perfectly ok to set/get any of these attributes directly.
But, it is also possible to apply configuration directly in the call to y: So, it is possible to say
from ycecream import y y(12, prefix="==> ")
, which will print
==> 12
It is also possible to configure y permanently with the configure method.
y.configure(prefix="==> ") y(12)
will print
==> 12
It is arguably easier to say:
y.prefix = "==> " y(12)
or even
y.p = "==> " y(12)
to print
==> 12
Yet another way to configure y is to get a new instance of y with y.new() and the required configuration:
z = y.new(prefix="==> ") z(12)
will print
==> 12
Or, yet another possibility is to clone y (optionally with modified attributes):
yd1 = y.clone(show_date=True) yd2 = y.clone() yd2.configure(show_date=True)
After this yd1 and yd2 will behave similarly (but they are not the same!)
prefix / p
from ycecream import y y('world', prefix='hello -> ')
prints
hello -> 'world'
prefix can be a function, too.
import time from ycecream import y def unix_timestamp(): return f"{int(time.time())} " hello = "world" y = Y(prefix=unix_timestamp) y(hello)
prints
1613635601 hello: 'world'
output / o
This will allow the output to be handled by something else than the default (output being written to stderr).
The output attribute can be
a callable that accepts at least one parameter (the text to be printed)
a string or Path object that will be used as the filename
a text file that is open for writing/appending
In the example below,
from ycecream import y import sys y(1, output=print) y(2, output=sys.stdout with open("test", "a+") as f: y(3, output=f) y(4, output="")
y| 1 will be printed to stdout
y| 2 will be printed to stdout
y| 3 will be appended to the file test
y| 4 will disappear
As output may be any callable, you can even use this to automatically log any y output:
from ycecream import y import logging logging.basicConfig(level="INFO") log = logging.getLogger("demo") y.configure(output=log.info) a = {1, 2, 3, 4, 5} y(a) a.remove(4) y(a)
will print to stderr:
INFO:demo:y| a: {1, 2, 3, 4, 5} INFO:demo:y| a: {1, 2, 3, 5}
Finally, you can specify the following strings:
"stderr" to print to stderr "stdput" to print to stdout "null" or "" to completely ignore (dummy) output "logging.debug" to use logging.debug "logging.info" to use logging.info "logging.warning" to use logging.warning "logging.error" to use logging.error "logging.critical" to use logging.critical
E.g.
from ycecream import y import sys y.configure(output="stdout")
to print to stdout.
serialize
This will allow to specify how argument values are to be serialized to displayable strings. The default is pformat (from pprint), but this can be changed to, for example, to handle non-standard datatypes in a custom fashion. The serialize function should accept at least one parameter. The function can optionally accept the keyword arguments width and sort_dicts, compact, indent and depth.
from ycecream import y def add_len(obj): if hasattr(obj, "__len__"): add = f" [len={len(obj)}]" else: add = "" return f"{repr(obj)}{add}" l = list(range(7)) hello = "world" y(7, hello, l, serialize=add_len)
prints
y| 7, hello: 'world' [len=5], l: [0, 1, 2, 3, 4, 5, 6] [len=7]
show_line_number / sln
If True, adds the y() call’s line number and possible the filename and parent function to y()’s output.
from ycecream import y y = Y(show_line_number=True) hello="world" y(hello)
prints like
y| #4 ==> hello: 'world'
Note that if you call y without any arguments, the line number is always shown, regardless of the status show_line_number.
See below for an explanation of the information provided.
show_time / st
If True, adds the current time to y()’s output.
from ycecream import y y = Y(show_time=True) hello="world" y(hello)
prints something like
y| @ 13:01:47.588125 ==> hello: 'world'
show_delta / sd
If True, adds the number of seconds since the start of the program to y()’s output.
from ycecream import y import time y.configure(show_delta=True) allô="monde" hallo y(hello) time.sleep(1) y(âllo)
prints something like
y| delta=0.021002 ==> hello: 'world' y| delta=1.053234 ==> âllo: 'monde'
show_enter / se
When used as a decorator or context manager, by default, ycecream ouputs a line when the decorated the function is called or the context manager is entered.
With show_enter=False this line can be suppressed.
show_exit / sx
When used as a decorator or context manager, by default, ycecream ouputs a line when the decorated the function returned or the context manager is exited.
With show_exit=False this line can be suppressed.
line_length / ll
This attribute is used to specify the line length (for wrapping). The default is 80. Ycecream always tries to keep all output on one line, but if it can’t it will wrap:
d = dict(a1=1,a2=dict(a=1,b=1,c=3),a3=list(range(10))) y(d) y(d, line_length=120)
prints
y| d: {'a1': 1, 'a2': {'a': 1, 'b': 1, 'c': 3}, 'a3': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]} y| d: {'a1': 1, 'a2': {'a': 1, 'b': 1, 'c': 3}, 'a3': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]}
compact / c
This attribute is used to specify the compact parameter for pformat (see the pprint documentation for details). compact is False by default.
a = 9 * ["0123456789"] y(a) y(a, compact=True)
prints
y| a: ['0123456789', '0123456789', '0123456789', '0123456789', '0123456789', '0123456789', '0123456789', '0123456789', '0123456789'] y| a: ['0123456789', '0123456789', '0123456789', '0123456789', '0123456789', '0123456789', '0123456789', '0123456789', '0123456789']
Note that compact is ignored under Python 2.7.
indent / i
This attribute is used to specify the indent parameter for pformat (see the pprint documentation for details). indent is 1 by default.
s = "01234567890012345678900123456789001234567890" y( [s, [s]]) y( [s, [s]], indent=4)
prints
y| [s, [s]]: ['01234567890012345678900123456789001234567890', ['01234567890012345678900123456789001234567890']] y| [s, [s]]: [ '01234567890012345678900123456789001234567890', ['01234567890012345678900123456789001234567890']]
depth / de
This attribute is used to specify the depth parameter for pformat (see the pprint documentation for details). depth is 1000000 by default.
s = "01234567890012345678900123456789001234567890" y([s,[s,[s,[s,s]]]]) y([s,[s,[s,[s,s]]]], depth=3)
prints
y| [s,[s,[s,[s,s]]]]: ['01234567890012345678900123456789001234567890', ['01234567890012345678900123456789001234567890', ['01234567890012345678900123456789001234567890', ['01234567890012345678900123456789001234567890', '01234567890012345678900123456789001234567890']]]] y| [s,[s,[s,[s,s]]]]: ['01234567890012345678900123456789001234567890', ['01234567890012345678900123456789001234567890', ['01234567890012345678900123456789001234567890', [...]]]]
wrap_indent / wi
This specifies the indent string if the output does not fit in the line_length (has to be wrapped). Rather than a string, wrap_indent can be also be an integer, in which case the wrap_indent will be that amount of blanks. The default is 4 blanks.
E.g.
d = dict(a1=1,a2=dict(a=1,b=1,c=3),a3=list(range(10))) y(d, wrap_indent=" ") y(d, wrap_indent="....") y(d, wrap_indent=2)
prints
y| d: {'a1': 1, 'a2': {'a': 1, 'b': 1, 'c': 3}, 'a3': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]} y| ....d: ........{'a1': 1, ........ 'a2': {'a': 1, 'b': 1, 'c': 3}, ........ 'a3': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]} y| d: {'a1': 1, 'a2': {'a': 1, 'b': 1, 'c': 3}, 'a3': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]}
enabled / e
Can be used to disable the output:
from ycecream import y y.configure(enabled=False) s = 'the world is ' y(s + 'perfect.') y.configure(enabled=True) y(s + 'on fire.')
prints
y| s + 'on fire.': 'the world is on fire.'
and nothing about a perfect world.
sort_dicts / sdi
By default, ycecream does not sort dicts (printed by pprint). However, it is possible to get the default pprint behaviour (i.e. sorting dicts) with the sorted_dicts attribute:
world = {"EN": "world", "NL": "wereld", "FR": "monde", "DE": "Welt"} y(world)) s1 = y(world, sort_dicts=False) s2 = y(world, sort_dicts=True)
prints
y| world: {'EN': 'world', 'NL': 'wereld', 'FR': 'monde', 'DE': 'Welt'} y| world: {'EN': 'world', 'NL': 'wereld', 'FR': 'monde', 'DE': 'Welt'} y| world: {'DE': 'Welt', 'EN': 'world', 'FR': 'monde', 'NL': 'wereld'}
Note that sort_dicts is ignored under Python 2.7, i.e. dicts are always sorted.
context_delimiter / cd
By default the line_number, time and/or delta are followed by ==>. It is possible to change this with the attribute context_delimiter:
a="abcd" y(a) y(a, show_time=True, context_delimiter = ' \u279c ')
prints:
y| @ 12:56:11.341650 ==> a: 'abcd' y| @ 12:56:11.485567 ➜ a: 'abcd'
pair_delimiter / pd
By default, pairs (on one line) are separated by ,. It is possible to change this with the attribute pair_delimiter:
a="abcd" b=1 c=1000 d=list("ycecream") y(a,(b,c),d) y(a,(b,c),d, pair_delimiter=" | ")
prints
y| a: 'abcd', (b,c): (1, 1000), d: ['y', 'c', 'e', 'c', 'r', 'e', 'a', 'm'] y| a: 'abcd' | (b,c): (1, 1000) | d: ['y', 'c', 'e', 'c', 'r', 'e', 'a', 'm']
values_only / vo
If False (the default), both the left-hand side (if possible) and the value will be printed. If True, the left_hand side will be suppressed:
hello = "world" y(hello, 2 * hello) y(hello, 2 * hello, values_only=True)
prints
y| hello: 'world', 2 * hello = 'worldworld' y| 'world', 'worldworld'
The values=True version of y can be seen as a supercharged print/pprint.
return_none / rn
Normally, y()returns the values passed directly, which is usually fine. However, when used in a notebook or REPL, that value will be shown, and that can be annoying. Therefore, if return_noneis True, y()will return None and thus not show anything.
a = 3 print(y(a, a + 1)) y.configure(return_none=True) print(y(a, a + 1))
prints
y| (3, 4) (3, 4) y| (3, 4) None
enforce_line_length / ell
If enforce_line_length is True, all output lines are explicitely truncated to the given line_length, even those that are not truncated by pformat.
decorator / d
Normally, an ycecream instance can be used as to show values, as a decorator and as a context manager.
However, when used from a REPL the usage as a decorator can’t be detected properly and in that case, specify decorator=True. E.g.
>>>@y(decorator=True) >>>def add2(x): >>> return x + 2 >>>print(add2(10)) y| called add2(10) y| returned 12 from add2(10) in 0.000548 seconds 12
The decorator attribute is also required when using y() as a decorator witb fast disabling (see below).
|y.enabled([]) |@y() |def add2(x): | return x + 2
would fail withTypeError: 'NoneType' object is not callable, but
|y.enabled([]) |@y(decorator=True) |def add2(x): | return x + 2
will run correctly.
context_manager / cm
Normally, an ycecream instance can be used as to show values, as a decorator and as a context manager.
However, when used from a REPL the usage as a context manager can’t be detected properly and in that case, specify context_manager=True. E.g.
>>>with y(context_manager=True) >>> pass y| enter y| exit in 0.008644 seconds
The context_manager attribute is also required when using y(): as a context manager witb fast disabling (see below).
|y.enabled([]) |with y: | pass
would fail with AttributeError: __enter__, but
|y.enabled([]) |with y(context_manager=True): | pass
will run correctly.
Return a string instead of sending to output
y(*args, as_str=True) is like y(*args) but the output is returned as a string instead of written to output.
from ycecream import y hello = "world" s = y(hello, as_str=True) print(s, end="")
prints
y| hello: 'world'
Disabling ycecream’s output
from ycecream import y yd = y.fork(show_delta=True) y(1) yd(2) y.enabled = False y(3) yd(4) y.enabled = True y(5) yd(6) print(y.enabled)
prints
y| 1 y| delta=0.011826 ==> 2 y| 5 y| delta=0.044893 ==> 6 True
Of course y() continues to return its arguments when disabled, of course.
Speeding up disabled ycecream
When output is disabled, either via y.configure(enbabled=False) or ycecream.enable = False, ycecream still has to check for usage as a decorator or context manager, which can be rather time consuming.
In order to speed up a program with disabled ycecream calls, it is possible to specify y.configure(enabled=[]), in which case y will always just return the given arguments. If ycecream is disabled this way, usage as a @y() decorator or as a with y(): context manager will raise a runtime error, though. The @y decorator without parentheses will not raise any exception, though.
To use y as a decorator and still want fast disabling:
y.configure(enabled=[]) @y(decorator=True): def add2(x): return x + 2 x34 = add2(30)
And, similarly, to use y as a context manager combined with fast disabling:
y.configure(enabled=[]) with @y(context_manager=True): pass
Note that calls with as_str=True will not be affected at all by the enabled flag.
The table below shows it all.
--------------------------------------------------------------------- enabled=True enabled=False enabled=[] --------------------------------------------------------------------- execution speed normal normal fast y() normal no output no output @y normal no output no output y(decorator=True) normal no output no output y(context_manager=True) normal no output no output @y() normal no output TypeError with y(): normal no output AttributeError y(as_str=True) normal normal normal ---------------------------------------------------------------------
Interpreting the line number information
When show_line_number is True or y() is used without any parameters, the output will contain the line number like:
y| #3 ==> a: 'abcd'
If the line resides in another file than the main file, the filename (without the path) will be shown as well:
y| #30[foo.py] ==> foo: 'Foo'
And finally when used in a function or method, that functiuon/method will be shown as well:
y| #456[foo.py] in square_root ==> x: 123
Configuring at import time
It can be useful to configure ycecream at import time. This can be done by providing a ycecream.json file which can contain any attribute configuration overriding the standard settings. E.g. if there is an ycecream.json file with the following contents
{ "o": "stdout", "show_time": true, "line_length": 120` 'compact' : true }
in the same folder as the application, this program:
from ycecream import y hello = "world" y(hello)
will print to stdout (rather than stderr):
y| @ 14:53:41.392190 ==> hello: 'world'
At import time the sys.path will be searched for, in that order, to find an ycecream.json file and use that. This mean that you can place an ycecream.json file in the site-packages folder where ycecream is installed to always use these modified settings.
Please observe that json values are slightly different from their Python equivalents:
------------------------------- Python json ------------------------------- True true False false None none strings always double quoted -------------------------------
Note that not-specified attributes will remain the default settings.
For obvious reasons, it is not possible to specify serialize in an ycecream.json file.
Working with multiple instances of y
Normally, only the y() object is used.
It can be useful to have multiple instances, e.g. when some of the debugging has to be done with context information and others requires an alternative prefix.
THere are several ways to obtain a new instance of ycecream:
by using y.new()
by using y.new(ignore_json=True)
by using y.fork()
by using y.clone(), which copies all attributes from y()
with y() used as a context manager
In either case, attributes can be added to override the default ones.
Example
from ycecream import y y_with_line_number = y.fork(show_line_number=True) y_with_new_prefix = y.new(prefix="==> ") y_with_new_prefix_and_time = y_with_new_prefix.clone(show_time=True) hello="world" y_with_line_number(hello) y_with_new_prefix(hello) y_with_new_prefix_and_time(hello) with y(prefix="ycm ") as ycm: ycm(hello) y(hello)
prints
y| x.py:6 ==> hello: 'world' ==> hello: 'world' ==> @ 10:15:41.457879 ==> hello: 'world' ycm enter ycm hello: 'world' y| hello: 'world' ycm exit in 0.041361 seconds
If you need to use y as such in your program you can always use
from ycecream import y as yc
The y.new(ignore_json=True) will return an instance of y without having applied any json configuration file. That can be useful when guaranteeing the same output in several setups.
Example
Suppose we have an ycecream.json file in the current directory with the contents
{prefix="==>"}
Then
y_post_json = y.new() y_ignore_json = y.new(ignore_json=True) hello = "world" y_post_json(hello) y_ignore_json(hello)
prints
==>hello: 'world' y| hello: 'world'
Test script
On GitHub is a file test_ycecream.py that tests (and thus also demonstrates) most of the functionality of ycecream.
It is very useful to have a look at the tests to see the features (some may be not covered (yet) in this readme).
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.