Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow using fire without editing code directly. #110

Closed
wants to merge 8 commits into from

Conversation

jtratner
Copy link
Contributor

@jtratner jtratner commented Jan 3, 2018

Enable python -m fire <module> <args> to work - closes #29. @dbieber

I wanted to restart the discussion in #29 with a very simple implementation that doesn't lock us into any advanced functionality at all. Everything else works exactly the same, except that you prefix with python -m fire and the first argument must resolve to a module.

python -m fire vs. fire

I used python -m fire rather than an external fire tool, because it's really important that the python interpreter be exactly the same, and this avoids confusing bugs where fire resolves to something in the global environment, but you're trying to test something in a virtualenv or conda environment.

Testing

I need to think a little bit about how to test running fire with -m (I'm sure someone on the internet knows) but I'd like to get feedback on whether this a good implementation before investing too much time there.

"Bugs"

  • --interactive currently jumps you into a shell with the specified module loaded, rather than what you might expect (from <module> import *)

Examples

$ python -m fire tempfile mkdtemp
/var/folders/fm/sjgpzb856kld04jvq82bxrcw0000gp/T/tmpCbfg_1

or

$ python -m fire urllib unquote genetics%3Dawesome%26editor%3Dcrispr
genetics=awesome&editor=crispr

This follows normal python import semantics, so if you have a script
file, it'll work if named just as a module:

# myscript.py
def fn1(x, y):
    return x
$ python -m fire myscript 5 6
5

Why manipulate sys.argv?

  1. when running with -m, nothing else is going to be pulling from sys.argv
  2. it makes for a nicer help text (rather than __main__.py you can show the module)
›› python -m fire base64
Type:        module
String form: <module 'base64' from '/usr/local/Cellar/python/2.7.13/Frameworks/Python.framework/Versions/2.7/lib/python2.7/base64.pyc'>
File:        /usr/local/Cellar/python/2.7.13/Frameworks/Python.framework/Versions/2.7/lib/python2.7/base64.py
Line:        1
Docstring:   RFC 3548: Base16, Base32, Base64 Data Encodings

Usage:       base64
             base64 EMPTYSTRING
             base64 MAXBINSIZE
             base64 MAXLINESIZE
             base64 b16decode
             base64 b16encode
             base64 b32decode
             base64 b32encode
             base64 b64decode
             base64 b64encode
             base64 binascii
             base64 decode
             base64 decodestring
             base64 encode
             base64 encodestring
             base64 k
             base64 re
             base64 standard-b64decode
             base64 standard-b64encode
             base64 string
             base64 struct
             base64 test
             base64 test1
             base64 urlsafe-b64decode
             base64 urlsafe-b64encode
             base64 v

@jtratner
Copy link
Contributor Author

jtratner commented Jan 3, 2018

The lint failures are all unrelated (my only change is to add fire.__main__ and they all are other modules)

@dbieber
Copy link
Member

dbieber commented Jan 4, 2018

Lint failures started unexpectedly recently, and I can confirm they're unrelated.

fire/__main__.py Outdated
module = sys.argv[1]
module = importlib.import_module(module)
sys.argv = sys.argv[1:2] + sys.argv[2:]
fire.Fire(module)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than manipulating sys.argv, you can pass the name= and command= arguments to fire.Fire.
name=module, command=sys.argv[2:].

@jtratner
Copy link
Contributor Author

jtratner commented Jan 6, 2018

@dbieber - added some test cases

def test_name_setting(self):
# confirm one of the usage lines has tempfile gettempdir
# with self.assertRaisesFireExit(2, 'tempfile gettempdir'):
with self.assertOutputMatches('tempfile gettempdir'):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused by the behavior here - argparse raises SystemExit(0,) if you say -h and SystemExit(2,) if not enough arguments, so I don't understand here (where it's printing out a usage statement) why it doesn't exit either 0 or 2 at this point.

(I think we've discussed this before too, but maybe I'm forgetting).

Commented out line is how I think it should work, but what actually works is uncommented.

Copy link
Member

@dbieber dbieber Jan 8, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's exiting with SystemExit(0), but not with a FireExit. Here are the current rules:

  • Whenever Fire encounters a FireError (this occurs when when a Fire command cannot be executed, and is different from user code raising an exception), Fire raises a FireExit with code 2 (Recall that FireExit is a subclass of SystemExit, and so the program exists).
  • When Fire is used with the help or trace flags, Fire raises a FireExit with code 0 if successful.
  • Otherwise (the default case), the program just ends, which is equivalent to a SystemExit(0), but not a FireExit

In the default case, Fire just returns (it doesn't raise the SystemExit explicitly). This allows a user to use the return value of the call to Fire if they want without having to catch an exception.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gotcha - I think I was expecting it to set a code of 2 just like argparse, but pretty bike-sheedy difference

fire/__main__.py Outdated

import fire

def main(args=None):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did this to make it easier to test

@dbieber
Copy link
Member

dbieber commented Jan 8, 2018

If you rebase, the lint errors are resolved now.

def main(args):
module_name = args[1]
module = importlib.import_module(module_name)
fire.Fire(module, name=module_name, command=args[2:])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want module or vars(module)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was hoping that vars(module) would make it so that interactive mode would look like from module import *, but that didn't work either.

My inclination would be to leave it as just module - seems like the inference should be for how python objects work by default, not for dictionaries (i.e., in terms of hiding "protected" attributes, etc).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah, my bad. I was also making that suggestion hoping it would behave like "import *".
Leaving it as module sgtm.

def test_name_setting(self):
# confirm one of the usage lines has tempfile gettempdir
# with self.assertRaisesFireExit(2, 'tempfile gettempdir'):
with self.assertOutputMatches('tempfile gettempdir'):
Copy link
Member

@dbieber dbieber Jan 8, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's exiting with SystemExit(0), but not with a FireExit. Here are the current rules:

  • Whenever Fire encounters a FireError (this occurs when when a Fire command cannot be executed, and is different from user code raising an exception), Fire raises a FireExit with code 2 (Recall that FireExit is a subclass of SystemExit, and so the program exists).
  • When Fire is used with the help or trace flags, Fire raises a FireExit with code 0 if successful.
  • Otherwise (the default case), the program just ends, which is equivalent to a SystemExit(0), but not a FireExit

In the default case, Fire just returns (it doesn't raise the SystemExit explicitly). This allows a user to use the return value of the call to Fire if they want without having to catch an exception.

with self.assertOutputMatches('%s\n' % expected):
__main__.main(['__main__.py', 'os.path', 'join', 'part1', 'part2', 'part3'])
with self.assertOutputMatches('%s\n' % expected):
__main__.main(['__Main__.py', 'os', 'path', '-', 'join', 'part1', 'part2', 'part3'])
Copy link
Member

@dbieber dbieber Jan 8, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix capitalization of '__main__.py'

Let users run fire as the main function to use fire to create CLIs
around *any* python module. This allows you to wrap third party
libraries with fire without writing any code! Exciting! :D

E.g.:

```
$ python -m fire tempfile mkdtemp
/var/folders/fm/sjgpzb856kld04jvq82bxrcw0000gp/T/tmpCbfg_1
```

or

```
$ python -m fire urllib unquote genetics%3Dawesome%26editor%3Dcrispr
genetics=awesome&editor=crispr
```

This follows normal python import semantics, so if you have a script
file, it'll work if named just as a module:

```
 # myscript.py
def fn1(x, y):
    return x
```

```
$ python -m fire myscript 5 6
5
```
@jtratner
Copy link
Contributor Author

jtratner commented Jan 9, 2018

@dbieber - I think this is ready for another round now - all the tests pass and I updated the test cases to reflect the description of fire's behavior.

Copy link
Member

@dbieber dbieber left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a handful of nits to satisfy the internal linter

from fire import testutils

class TestAsMainModule(testutils.BaseTestCase):
def test_name_setting(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: use testCamelCase for test names.

from fire import __main__
from fire import testutils

class TestAsMainModule(testutils.BaseTestCase):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: "MainModuleTest"

def main(args):
module_name = args[1]
module = importlib.import_module(module_name)
fire.Fire(module, name=module_name, command=args[2:])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah, my bad. I was also making that suggestion hoping it would behave like "import *".
Leaving it as module sgtm.

fire/__main__.py Outdated
@@ -0,0 +1,16 @@
"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry to be so picky; have to satisfy our internal linter.
Docstring/header format should be one of:

"""One line docstring.

Rest of docstring.
"""

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

or

"""One line docstring."""

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

# confirm one of the usage lines has tempfile gettempdir
with self.assertOutputMatches('tempfile gettempdir'):
__main__.main(['__main__.py', 'tempfile'])
def test_arg_passing(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: blank line before each "def" in the class, and two blank lines before the "class" line at line 9.

@jtratner
Copy link
Contributor Author

okay @dbieber - I think this should fix all the lint issues you cited

@jtratner
Copy link
Contributor Author

@dbieber - ping if you've got a moment :)

@jtratner
Copy link
Contributor Author

jtratner commented Feb 23, 2018 via email

@dbieber
Copy link
Member

dbieber commented Feb 23, 2018

Will take a look post-release. Apologies for missing your previous ping.

@dbieber dbieber added the cla: yes Author has signed CLA label Feb 28, 2020
python-fire-bot pushed a commit that referenced this pull request Mar 6, 2020
Let users run fire as the main function to use fire to create CLIs
around *any* python module. This allows you to wrap third party
libraries with fire without writing any code! Exciting! :D

E.g.:

```
$ python -m fire tempfile mkdtemp
/var/folders/fm/sjgpzb856kld04jvq82bxrcw0000gp/T/tmpCbfg_1
```

or

```
$ python -m fire urllib unquote genetics%3Dawesome%26editor%3Dcrispr
genetics=awesome&editor=crispr
```

This follows normal python import semantics, so if you have a script
file, it'll work if named just as a module:

```
 # myscript.py
def fn1(x, y):
    return x
```

COPYBARA_INTEGRATE_REVIEW=#110 from jtratner:main-fire dbe70e2
PiperOrigin-RevId: 299450705
Change-Id: I9ebead46b141657d826ca81a787e15d6f8d9c9b8
@dbieber
Copy link
Member

dbieber commented Mar 6, 2020

Alright, we're doing it! 598937e

We'll include this in the next release.

@dbieber dbieber closed this Mar 6, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cla: yes Author has signed CLA
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Use fire without editing code directly
2 participants