Skip to content

Commit

Permalink
Make ipython fully optional (google#78)
Browse files Browse the repository at this point in the history
* Fall back on custom info function if IPython's oinspect is not available, thereby making IPython an optional dependency.
* Test IPython if IPython available, otherwise test with 'code' module.
  • Loading branch information
dbieber authored May 25, 2017
1 parent 3126e2c commit 952e20d
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 43 deletions.
4 changes: 3 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,7 @@ before_install:
install:
- python setup.py develop
script:
- python -m pytest
- python -m pytest # Run the tests without IPython.
- pip install ipython
- python -m pytest # Now run the tests with IPython.
- if [[ $TRAVIS_PYTHON_VERSION != 3.6 ]]; then pylint fire --ignore=test_components_py3.py,parser_fuzz_test.py; fi
31 changes: 2 additions & 29 deletions fire/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ def _Fire(component, args, context, name=None):

try:
target = component.__name__
filename, lineno = _GetFileAndLine(component)
filename, lineno = inspectutils.GetFileAndLine(component)

component, consumed_args, remaining_args, capacity = _CallCallable(
component, remaining_args)
Expand Down Expand Up @@ -428,7 +428,7 @@ def _Fire(component, args, context, name=None):
component, consumed_args, remaining_args = _GetMember(
component, remaining_args)

filename, lineno = _GetFileAndLine(component)
filename, lineno = inspectutils.GetFileAndLine(component)

component_trace.AddAccessedProperty(
component, target, consumed_args, filename, lineno)
Expand Down Expand Up @@ -486,33 +486,6 @@ def _Fire(component, args, context, name=None):
return component_trace


def _GetFileAndLine(component):
"""Returns the filename and line number of component.
Args:
component: A component to find the source information for, usually a class
or routine.
Returns:
filename: The name of the file where component is defined.
lineno: The line number where component is defined.
"""
if inspect.isbuiltin(component):
return None, None

try:
filename = inspect.getsourcefile(component)
except TypeError:
return None, None

try:
unused_code, lineindex = inspect.findsource(component)
lineno = lineindex + 1
except IOError:
lineno = None

return filename, lineno


def _GetMember(component, args):
"""Returns a subcomponent of component by consuming an arg from args.
Expand Down
3 changes: 2 additions & 1 deletion fire/helputils_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ def testHelpStringObject(self):
self.assertIn('Type: NoDefaults', helpstring)
self.assertIn('String form: <fire.test_components.NoDefaults object at ',
helpstring)
self.assertIn('test_components.py', helpstring)
# TODO: We comment this out since it only works with IPython:
# self.assertIn('test_components.py', helpstring)
self.assertIn('Usage: double\n'
' triple', helpstring)

Expand Down
67 changes: 64 additions & 3 deletions fire/inspectutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,33 @@ def GetFullArgSpec(fn):
kwonlyargs, kwonlydefaults, annotations)


def GetFileAndLine(component):
"""Returns the filename and line number of component.
Args:
component: A component to find the source information for, usually a class
or routine.
Returns:
filename: The name of the file where component is defined.
lineno: The line number where component is defined.
"""
if inspect.isbuiltin(component):
return None, None

try:
filename = inspect.getsourcefile(component)
except TypeError:
return None, None

try:
unused_code, lineindex = inspect.findsource(component)
lineno = lineindex + 1
except IOError:
lineno = None

return filename, lineno


def Info(component):
"""Returns a dict with information about the given component.
Expand All @@ -130,9 +157,12 @@ def Info(component):
Returns:
A dict with information about the component.
"""
import IPython # pylint: disable=g-import-not-at-top
inspector = IPython.core.oinspect.Inspector()
info = inspector.info(component)
try:
from IPython.core import oinspect # pylint: disable=g-import-not-at-top
inspector = oinspect.Inspector()
info = inspector.info(component)
except ImportError:
info = _InfoBackup(component)

try:
unused_code, lineindex = inspect.findsource(component)
Expand All @@ -141,3 +171,34 @@ def Info(component):
info['line'] = None

return info


def _InfoBackup(component):
"""Returns a dict with information about the given component.
This function is to be called only in the case that IPython's
oinspect module is not available. The info dict it produces may
contain less information that contained in the info dict produced
by oinspect.
Args:
component: The component to analyze.
Returns:
A dict with information about the component.
"""
info = {}

info['type_name'] = type(component).__name__
info['string_form'] = str(component)

filename, lineno = GetFileAndLine(component)
info['file'] = filename
info['line'] = lineno
info['docstring'] = inspect.getdoc(component)

try:
info['length'] = str(len(component))
except (TypeError, AttributeError):
pass

return info
23 changes: 15 additions & 8 deletions fire/interact_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,29 @@
import mock


try:
import IPython # pylint: disable=unused-import, g-import-not-at-top
INTERACT_METHOD = 'IPython.start_ipython'
except ImportError:
INTERACT_METHOD = 'code.InteractiveConsole'


class InteractTest(testutils.BaseTestCase):

@mock.patch('IPython.start_ipython')
def testInteract(self, mock_ipython):
self.assertFalse(mock_ipython.called)
@mock.patch(INTERACT_METHOD)
def testInteract(self, mock_interact_method):
self.assertFalse(mock_interact_method.called)
interact.Embed({})
self.assertTrue(mock_ipython.called)
self.assertTrue(mock_interact_method.called)

@mock.patch('IPython.start_ipython')
def testInteractVariables(self, mock_ipython):
self.assertFalse(mock_ipython.called)
@mock.patch(INTERACT_METHOD)
def testInteractVariables(self, mock_interact_method):
self.assertFalse(mock_interact_method.called)
interact.Embed({
'count': 10,
'mock': mock,
})
self.assertTrue(mock_ipython.called)
self.assertTrue(mock_interact_method.called)

if __name__ == '__main__':
testutils.main()
1 change: 0 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
A library for automatically generating command line interfaces.""".strip()

DEPENDENCIES = [
'ipython<6.0',
'six',
]

Expand Down

0 comments on commit 952e20d

Please sign in to comment.