Skip to content

Commit

Permalink
Merge pull request #136 from ehudhala/feature/print_actual_args_even_…
Browse files Browse the repository at this point in the history
…when_source_unavailable

We want to still print the given args even when the source is unavaii…
  • Loading branch information
alexmojaki authored Dec 4, 2022
2 parents f99c74a + ecf1e0b commit fc40981
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 53 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ on:

jobs:
build:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
strategy:
matrix:
include:
Expand Down
89 changes: 47 additions & 42 deletions icecream/icecream.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import inspect
import pprint
import sys
import warnings
from datetime import datetime
import functools
from contextlib import contextmanager
Expand All @@ -37,8 +38,8 @@

PYTHON2 = (sys.version_info[0] == 2)


_absent = object()
_arg_source_missing = object()


def bindStaticVariable(name, value):
Expand Down Expand Up @@ -90,25 +91,25 @@ def colorizedStderrPrint(s):
DEFAULT_ARG_TO_STRING_FUNCTION = pprint.pformat


class NoSourceAvailableError(OSError):
"""
Raised when icecream fails to find or access source code that's
required to parse and analyze. This can happen, for example, when
"""
This info message is printed instead of the arguments when icecream
fails to find or access source code that's required to parse and analyze.
This can happen, for example, when
- ic() is invoked inside a REPL or interactive shell, e.g. from the
command line (CLI) or with python -i.
- ic() is invoked inside a REPL or interactive shell, e.g. from the
command line (CLI) or with python -i.
- The source code is mangled and/or packaged, e.g. with a project
freezer like PyInstaller.
- The source code is mangled and/or packaged, e.g. with a project
freezer like PyInstaller.
- The underlying source code changed during execution. See
https://stackoverflow.com/a/33175832.
"""
infoMessage = (
'Failed to access the underlying source code for analysis. Was ic() '
'invoked in a REPL (e.g. from the command line), a frozen application '
'(e.g. packaged with PyInstaller), or did the underlying source code '
'change during execution?')
- The underlying source code changed during execution. See
https://stackoverflow.com/a/33175832.
"""
NO_SOURCE_AVAILABLE_WARNING_MESSAGE = (
'Failed to access the underlying source code for analysis. Was ic() '
'invoked in a REPL (e.g. from the command line), a frozen application '
'(e.g. packaged with PyInstaller), or did the underlying source code '
'change during execution?')


def callOrValue(obj):
Expand Down Expand Up @@ -143,8 +144,12 @@ def indented_lines(prefix, string):


def format_pair(prefix, arg, value):
arg_lines = indented_lines(prefix, arg)
value_prefix = arg_lines[-1] + ': '
if arg is _arg_source_missing:
arg_lines = []
value_prefix = prefix
else:
arg_lines = indented_lines(prefix, arg)
value_prefix = arg_lines[-1] + ': '

looksLikeAString = value[0] + value[-1] in ["''", '""']
if looksLikeAString: # Align the start of multiline strings.
Expand Down Expand Up @@ -204,12 +209,7 @@ def __init__(self, prefix=DEFAULT_PREFIX,
def __call__(self, *args):
if self.enabled:
callFrame = inspect.currentframe().f_back
try:
out = self._format(callFrame, *args)
except NoSourceAvailableError as err:
prefix = callOrValue(self.prefix)
out = prefix + 'Error: ' + err.infoMessage
self.outputFunction(out)
self.outputFunction(self._format(callFrame, *args))

if not args: # E.g. ic().
passthrough = None
Expand All @@ -228,27 +228,29 @@ def format(self, *args):
def _format(self, callFrame, *args):
prefix = callOrValue(self.prefix)

callNode = Source.executing(callFrame).node
if callNode is None:
raise NoSourceAvailableError()

context = self._formatContext(callFrame, callNode)
context = self._formatContext(callFrame)
if not args:
time = self._formatTime()
out = prefix + context + time
else:
if not self.includeContext:
context = ''
out = self._formatArgs(
callFrame, callNode, prefix, context, args)
callFrame, prefix, context, args)

return out

def _formatArgs(self, callFrame, callNode, prefix, context, args):
source = Source.for_frame(callFrame)
sanitizedArgStrs = [
source.get_text_with_indentation(arg)
for arg in callNode.args]
def _formatArgs(self, callFrame, prefix, context, args):
callNode = Source.executing(callFrame).node
if callNode is not None:
source = Source.for_frame(callFrame)
sanitizedArgStrs = [
source.get_text_with_indentation(arg)
for arg in callNode.args]
else:
warnings.warn(NO_SOURCE_AVAILABLE_WARNING_MESSAGE,
category=RuntimeWarning, stacklevel=4)
sanitizedArgStrs = [_arg_source_missing] * len(args)

pairs = list(zip(sanitizedArgStrs, args))

Expand All @@ -271,8 +273,12 @@ def argPrefix(arg):
#
# ic| "hello": 'hello'.
#
# When the source for an arg is missing we also only print the value,
# since we can't know anything about the argument itself.
pairStrs = [
val if isLiteral(arg) else (argPrefix(arg) + val)
val
if (isLiteral(arg) or arg is _arg_source_missing)
else (argPrefix(arg) + val)
for arg, val in pairs]

allArgsOnOneLine = self._pairDelimiter.join(pairStrs)
Expand Down Expand Up @@ -313,9 +319,8 @@ def argPrefix(arg):

return '\n'.join(lines)

def _formatContext(self, callFrame, callNode):
filename, lineNumber, parentFunction = self._getContext(
callFrame, callNode)
def _formatContext(self, callFrame):
filename, lineNumber, parentFunction = self._getContext(callFrame)

if parentFunction != '<module>':
parentFunction = '%s()' % parentFunction
Expand All @@ -328,9 +333,9 @@ def _formatTime(self):
formatted = now.strftime('%H:%M:%S.%f')[:-3]
return ' at %s' % formatted

def _getContext(self, callFrame, callNode):
lineNumber = callNode.lineno
def _getContext(self, callFrame):
frameInfo = inspect.getframeinfo(callFrame)
lineNumber = frameInfo.lineno
parentFunction = frameInfo.function

filepath = (realpath if self.contextAbsPath else basename)(frameInfo.filename)
Expand Down
45 changes: 35 additions & 10 deletions tests/test_icecream.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import functools
import sys
import unittest
import warnings

try: # Python 2.x.
from StringIO import StringIO
except ImportError: # Python 3.x.
Expand All @@ -21,8 +23,7 @@
from os.path import basename, splitext, realpath

import icecream
from icecream import ic, argumentToString, stderrPrint, NoSourceAvailableError

from icecream import ic, argumentToString, stderrPrint, NO_SOURCE_AVAILABLE_WARNING_MESSAGE

TEST_PAIR_DELIMITER = '| '
MY_FILENAME = basename(__file__)
Expand Down Expand Up @@ -183,12 +184,13 @@ def parseOutputIntoPairs(out, err, assertNumLines,
if len(pairs[0]) == 1 and line.startswith(' '):
arg, value = linePairs[-1][-1]
looksLikeAString = value[0] in ["'", '"']
prefix = (arg + ': ') + (' ' if looksLikeAString else '')
prefix = ((arg + ': ' if arg is not None else '') # A multiline value
+ (' ' if looksLikeAString else ''))
dedented = line[len(ic.prefix) + len(prefix):]
linePairs[-1][-1] = (arg, value + '\n' + dedented)
else:
items = [
(p[0].strip(), None) if len(p) == 1 # A value, like ic(3).
(None, p[0].strip()) if len(p) == 1 # A value, like ic(3).
else (p[0].strip(), p[1].strip()) # A variable, like ic(a).
for p in pairs]
linePairs.append(items)
Expand Down Expand Up @@ -251,7 +253,7 @@ def testNestedMultiline(self):
ic(a,
'foo')
pairs = parseOutputIntoPairs(out, err, 1)[0]
assert pairs == [('a', '1'), ("'foo'", None)]
assert pairs == [('a', '1'), (None, "'foo'")]

with disableColoring(), captureStandardStreams() as (out, err):
noop(noop(noop({1: ic(
Expand Down Expand Up @@ -478,7 +480,7 @@ def testValues(self):
ic(3, 'asdf', "asdf")

pairs = parseOutputIntoPairs(out, err, 1)
assert pairs == [[('3', None), ("'asdf'", None), ("'asdf'", None)]]
assert pairs == [[(None, '3'), (None, "'asdf'"), (None, "'asdf'")]]

def testIncludeContextMultiLine(self):
multilineStr = 'line1\nline2'
Expand Down Expand Up @@ -527,10 +529,33 @@ def testMultilineInvocationWithComments(self):
pairs = parseOutputIntoPairs(out, err, 1)[0]
assert pairs == [('a', '1'), ('b', '2')]

def testNoSourceAvailable(self):
with disableColoring(), captureStandardStreams() as (out, err):
eval('ic()')
assert NoSourceAvailableError.infoMessage in err.getvalue()
def testNoSourceAvailablePrintsValues(self):
with disableColoring(), captureStandardStreams() as (out, err), warnings.catch_warnings():
# we ignore the warning so that it doesn't interfere with parsing ic's output
warnings.simplefilter("ignore")
eval('ic(a, b)')
pairs = parseOutputIntoPairs(out, err, 1)
self.assertEqual(pairs, [[(None, '1'), (None, "2")]])

def testNoSourceAvailablePrintsMultiline(self):
"""
This tests for a bug which caused only multiline prints to fail.
"""
multilineStr = 'line1\nline2'
with disableColoring(), captureStandardStreams() as (out, err), warnings.catch_warnings():
# we ignore the warning so that it doesn't interfere with parsing ic's output
warnings.simplefilter("ignore")
eval('ic(multilineStr)')
pair = parseOutputIntoPairs(out, err, 2)[0][0]
self.assertEqual(pair, (None, ic.argToStringFunction(multilineStr)))

def testNoSourceAvailableIssuesExactlyOneWarning(self):
with warnings.catch_warnings(record=True) as all_warnings:
eval('ic(a)')
eval('ic(b)')
assert len(all_warnings) == 1
warning = all_warnings[-1]
assert NO_SOURCE_AVAILABLE_WARNING_MESSAGE in str(warning.message)

def testSingleTupleArgument(self):
with disableColoring(), captureStandardStreams() as (out, err):
Expand Down

0 comments on commit fc40981

Please sign in to comment.