-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
Conversation
The lint failures are all unrelated (my only change is to add |
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) |
There was a problem hiding this comment.
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:]
.
@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'): |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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
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:]) |
There was a problem hiding this comment.
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)
?
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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'): |
There was a problem hiding this comment.
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.
fire/test_main.py
Outdated
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']) |
There was a problem hiding this comment.
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 ```
@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. |
There was a problem hiding this 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
fire/test_main.py
Outdated
from fire import testutils | ||
|
||
class TestAsMainModule(testutils.BaseTestCase): | ||
def test_name_setting(self): |
There was a problem hiding this comment.
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.
fire/test_main.py
Outdated
from fire import __main__ | ||
from fire import testutils | ||
|
||
class TestAsMainModule(testutils.BaseTestCase): |
There was a problem hiding this comment.
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:]) |
There was a problem hiding this comment.
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 @@ | |||
""" |
There was a problem hiding this comment.
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
fire/test_main.py
Outdated
# confirm one of the usage lines has tempfile gettempdir | ||
with self.assertOutputMatches('tempfile gettempdir'): | ||
__main__.main(['__main__.py', 'tempfile']) | ||
def test_arg_passing(self): |
There was a problem hiding this comment.
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.
okay @dbieber - I think this should fix all the lint issues you cited |
@dbieber - ping if you've got a moment :) |
@dbieber - any remaining steps to merge this? :)
…On Tue, Jan 9, 2018 at 2:00 PM David Bieber ***@***.***> wrote:
***@***.**** commented on this pull request.
a handful of nits to satisfy the internal linter
------------------------------
In fire/test_main.py
<#110 (comment)>:
> @@ -0,0 +1,21 @@
+"""
+Test using fire via python -m fire
+"""
+import os
+
+from fire import __main__
+from fire import testutils
+
+class TestAsMainModule(testutils.BaseTestCase):
+ def test_name_setting(self):
nit: use testCamelCase for test names.
------------------------------
In fire/test_main.py
<#110 (comment)>:
> @@ -0,0 +1,21 @@
+"""
+Test using fire via python -m fire
+"""
+import os
+
+from fire import __main__
+from fire import testutils
+
+class TestAsMainModule(testutils.BaseTestCase):
nit: "MainModuleTest"
------------------------------
In fire/__main__.py
<#110 (comment)>:
> @@ -0,0 +1,16 @@
+"""
+Module for running python fire as a "main" function and thereby allowing using
+fire with other programs.
+"""
+import importlib
+import sys
+
+import fire
+
+def main(args):
+ module_name = args[1]
+ module = importlib.import_module(module_name)
+ fire.Fire(module, name=module_name, command=args[2:])
Oh yeah, my bad. I was also making that suggestion hoping it would behave
like "import *".
Leaving it as module sgtm.
------------------------------
In fire/__main__.py
<#110 (comment)>:
> @@ -0,0 +1,16 @@
+"""
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
------------------------------
In fire/test_main.py
<#110 (comment)>:
> @@ -0,0 +1,21 @@
+"""
+Test using fire via python -m fire
+"""
+import os
+
+from fire import __main__
+from fire import testutils
+
+class TestAsMainModule(testutils.BaseTestCase):
+ def test_name_setting(self):
+ # confirm one of the usage lines has tempfile gettempdir
+ with self.assertOutputMatches('tempfile gettempdir'):
+ __main__.main(['__main__.py', 'tempfile'])
+ def test_arg_passing(self):
nit: blank line before each "def" in the class, and two blank lines before
the "class" line at line 9.
—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
<#110 (review)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/ABhjq_5BIdDEVGeN6t2vcXuE8klqlTVaks5tI-GLgaJpZM4RRTcN>
.
|
Will take a look post-release. Apologies for missing your previous ping. |
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
Alright, we're doing it! 598937e We'll include this in the next release. |
Enable
python -m fire <module> <args>
to work - closes #29. @dbieberI 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 externalfire
tool, because it's really important that the python interpreter be exactly the same, and this avoids confusing bugs wherefire
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
or
This follows normal python import semantics, so if you have a script
file, it'll work if named just as a module:
Why manipulate sys.argv?
-m
, nothing else is going to be pulling from sys.argv__main__.py
you can show the module)