Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,42 @@ example.py -- --help` or `python example.py --help` (or even `python example.py
The complete set of flags available is shown below, in the reference section.


### Environment Variable Configuration

Python Fire supports reading configuration values from environment variables.
You can specify a value for any parameter using the `FIRE_<PARAM_NAME>`
naming convention, where `<PARAM_NAME>` is the uppercase name of the parameter.
For example, if you have a parameter named `name`, you can set its value using
the `FIRE_NAME` environment variable.

Environment variables can be used for positional arguments, named arguments, and
even arguments that are captured by `**kwargs`.

Values provided via the command line (either positionally or as flags) take
precedence over values provided via environment variables.

Example:
```bash
$ export FIRE_NAME=World
$ python example.py hello
Hello World!
$ python example.py hello --name=Fire
Hello Fire!
```

Values in environment variables are parsed as Python literals, just like command
line arguments.
```bash
$ export FIRE_COUNT=5
$ python example.py repeat "Hello"
Hello
Hello
Hello
Hello
Hello
```


### Reference

| Setup | Command | Notes
Expand Down
17 changes: 17 additions & 0 deletions docs/using-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,23 @@ by name, using the flags syntax. See the section on
Similarly, when passing arguments to a callable object (an object with a custom
`__call__` function), those arguments must be passed using flags syntax.


### Environment Variable Configuration

Python Fire supports reading configuration values from environment variables.
Any parameter can be specified using the `FIRE_<PARAM_NAME>` naming convention,
where `<PARAM_NAME>` is the uppercase name of the parameter.

For example, if a function has a parameter named `name`, its value can be set
using the `FIRE_NAME` environment variable.

Values provided via the command line (either positionally or as flags) take
precedence over values provided via environment variables.

Values in environment variables are parsed as Python literals, just like command
line arguments.


## Using Flags with Fire CLIs <a name="using-flags"></a>

Command line arguments to a Fire CLI are normally consumed by Fire, as described
Expand Down
37 changes: 32 additions & 5 deletions fire/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,21 @@ def _ParseFn(args):
"""Parses the list of `args` into (varargs, kwargs), remaining_args."""
kwargs, remaining_kwargs, remaining_args = _ParseKeywordArgs(args, fn_spec)

# Read configuration values from environment variables for kwonlyargs.
for arg_name in fn_spec.kwonlyargs:
if arg_name not in kwargs:
env_var = 'FIRE_{name}'.format(name=arg_name.upper())
if env_var in os.environ:
kwargs[arg_name] = os.environ[env_var]

# If varkw is present, also read other FIRE_ environment variables.
if fn_spec.varkw:
for env_name, env_val in os.environ.items():
if env_name.startswith('FIRE_'):
param_name = env_name[5:].lower()
if param_name not in kwargs and param_name not in fn_spec.args:
kwargs[param_name] = env_val

# Note: _ParseArgs modifies kwargs.
parsed_args, kwargs, remaining_args, capacity = _ParseArgs(
fn_spec.args, fn_spec.defaults, num_required_args, kwargs,
Expand Down Expand Up @@ -804,14 +819,26 @@ def _ParseArgs(fn_args, fn_defaults, num_required_args, kwargs,
value = _ParseValue(value, index, arg, metadata)
parsed_args.append(value)
elif index < num_required_args:
raise FireError(
'The function received no value for the required argument:', arg)
env_var = 'FIRE_{name}'.format(name=arg.upper())
if env_var in os.environ:
value = os.environ[env_var]
value = _ParseValue(value, index, arg, metadata)
parsed_args.append(value)
else:
raise FireError(
'The function received no value for the required argument:', arg)
else:
# We're past the args for which there's no default value.
# There's a default value for this arg.
capacity = True
default_index = index - num_required_args # index into the defaults.
parsed_args.append(fn_defaults[default_index])
env_var = 'FIRE_{name}'.format(name=arg.upper())
if env_var in os.environ:
value = os.environ[env_var]
value = _ParseValue(value, index, arg, metadata)
parsed_args.append(value)
else:
capacity = True
default_index = index - num_required_args # index into the defaults.
parsed_args.append(fn_defaults[default_index])

for key, value in kwargs.items():
kwargs[key] = _ParseValue(value, None, key, metadata)
Expand Down
102 changes: 102 additions & 0 deletions fire/env_config_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Copyright (C) 2018 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Tests for environment variable configuration support."""

import os
from unittest import mock

from fire import core
from fire import testutils


class EnvVarTest(testutils.BaseTestCase):

def testEnvVarConfig(self):
def fn(arg1, arg2=10):
return arg1, arg2

# Without env vars, it should fail if required arg is missing.
with self.assertRaisesFireExit(2, 'The function received no value for the required argument: arg1'):
core.Fire(fn, command=[])

# With env var for arg1.
with mock.patch.dict(os.environ, {'FIRE_ARG1': 'hello'}):
self.assertEqual(core.Fire(fn, command=[]), ('hello', 10))

# With env var for arg2.
with mock.patch.dict(os.environ, {'FIRE_ARG2': '20'}):
# Note: it will still fail for arg1 if missing.
with self.assertRaisesFireExit(2, 'The function received no value for the required argument: arg1'):
core.Fire(fn, command=[])

# Now provide arg1 on CLI.
self.assertEqual(core.Fire(fn, command=['hi']), ('hi', 20))

# Env var for both.
with mock.patch.dict(os.environ, {'FIRE_ARG1': 'val1', 'FIRE_ARG2': 'val2'}):
self.assertEqual(core.Fire(fn, command=[]), ('val1', 'val2'))

# Command line overrides env var.
with mock.patch.dict(os.environ, {'FIRE_ARG1': 'env1', 'FIRE_ARG2': 'env2'}):
self.assertEqual(core.Fire(fn, command=['cli1']), ('cli1', 'env2'))
self.assertEqual(core.Fire(fn, command=['--arg2=cli2']), ('env1', 'cli2'))

def testEnvVarWithUnderscores(self):
def fn(my_arg, another_arg=1):
return my_arg, another_arg

with mock.patch.dict(os.environ, {'FIRE_MY_ARG': 'val1', 'FIRE_ANOTHER_ARG': 'val2'}):
self.assertEqual(core.Fire(fn, command=[]), ('val1', 'val2'))

def testEnvVarWithKwonly(self):
def fn(*, arg1, arg2=10):
return arg1, arg2

with mock.patch.dict(os.environ, {'FIRE_ARG1': 'hello'}):
self.assertEqual(core.Fire(fn, command=[]), ('hello', 10))

with mock.patch.dict(os.environ, {'FIRE_ARG1': 'v1', 'FIRE_ARG2': 'v2'}):
self.assertEqual(core.Fire(fn, command=[]), ('v1', 'v2'))
self.assertEqual(core.Fire(fn, command=['--arg2=v3']), ('v1', 'v3'))

def testEnvVarWithVarkw(self):
def fn(**kwargs):
return kwargs

with mock.patch.dict(os.environ, {'FIRE_ARG1': 'val1', 'FIRE_ARG2': 'val2'}):
self.assertEqual(core.Fire(fn, command=[]), {'arg1': 'val1', 'arg2': 'val2'})

def testEnvVarTypes(self):
def fn(arg1):
return arg1, type(arg1)

# Boolean
with mock.patch.dict(os.environ, {'FIRE_ARG1': 'True'}):
self.assertEqual(core.Fire(fn, command=[]), (True, bool))

# Integer
with mock.patch.dict(os.environ, {'FIRE_ARG1': '123'}):
self.assertEqual(core.Fire(fn, command=[]), (123, int))

# List
with mock.patch.dict(os.environ, {'FIRE_ARG1': '[1, 2, 3]'}):
self.assertEqual(core.Fire(fn, command=[]), ([1, 2, 3], list))

# Dict
with mock.patch.dict(os.environ, {'FIRE_ARG1': '{"a": 1}'}):
self.assertEqual(core.Fire(fn, command=[]), ({'a': 1}, dict))

if __name__ == '__main__':
testutils.main()