Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat(cli): add --env and --env-file support to improve test coverage #384

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
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
8 changes: 8 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[run]
omit =
src/functions_framework/_http/gunicorn.py
src/functions_framework/request_timeout.py

[report]
exclude_lines =
pragma: no cover
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ dist/
function_output.json
serverlog_stderr.txt
serverlog_stdout.txt
venv/
venv/
36 changes: 34 additions & 2 deletions src/functions_framework/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,42 @@
is_flag=True,
help="Use ASGI server for function execution",
)
def _cli(target, source, signature_type, host, port, debug, asgi):
@click.option(
"--env",
multiple=True,
help="Set environment variables (can be used multiple times): --env KEY=VALUE",
)
@click.option(
"--env-file",
multiple=True,
type=click.Path(exists=True),
help="Path(s) to file(s) containing environment variables (KEY=VALUE format)",
)
def _cli(target, source, signature_type, host, port, debug, asgi, env, env_file):
# Load environment variables from all provided --env-file arguments
for file_path in env_file:
with open(file_path, "r") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue # Skip comments and blank lines
if "=" in line:
key, value = line.split("=", 1)
os.environ[key.strip()] = value.strip()
else:
raise click.BadParameter(f"Invalid line in env-file '{file_path}': {line}")

# Load environment variables from all --env flags
for item in env:
if "=" in item:
key, value = item.split("=", 1)
os.environ[key.strip()] = value.strip()
else:
raise click.BadParameter(f"Invalid --env format: '{item}'. Expected KEY=VALUE.")

# Launch ASGI or WSGI server
if asgi: # pragma: no cover
from functions_framework.aio import create_asgi_app

app = create_asgi_app(target, source, signature_type)
else:
app = create_app(target, source, signature_type)
Expand Down
105 changes: 104 additions & 1 deletion tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# limitations under the License.

import sys
import os

import pretend
import pytest
Expand All @@ -27,7 +28,6 @@
def test_cli_no_arguments():
runner = CliRunner()
result = runner.invoke(_cli)

assert result.exit_code == 2
assert "Missing option '--target'" in result.output

Expand Down Expand Up @@ -124,3 +124,106 @@ def test_asgi_cli(monkeypatch):
assert result.exit_code == 0
assert create_asgi_app.calls == [pretend.call("foo", None, "http")]
assert asgi_server.run.calls == [pretend.call("0.0.0.0", 8080)]


def test_cli_sets_env(monkeypatch):
wsgi_server = pretend.stub(run=pretend.call_recorder(lambda *a, **kw: None))
wsgi_app = pretend.stub(run=pretend.call_recorder(lambda *a, **kw: None))
create_app = pretend.call_recorder(lambda *a, **kw: wsgi_app)
monkeypatch.setattr(functions_framework._cli, "create_app", create_app)
create_server = pretend.call_recorder(lambda *a, **kw: wsgi_server)
monkeypatch.setattr(functions_framework._cli, "create_server", create_server)

runner = CliRunner()
result = runner.invoke(
_cli,
["--target", "foo", "--env", "API_KEY=123", "--env", "MODE=dev"]
)

assert result.exit_code == 0
assert create_app.calls == [pretend.call("foo", None, "http")]
assert wsgi_server.run.calls == [pretend.call("0.0.0.0", 8080)]
# Check environment variables are set
assert os.environ["API_KEY"] == "123"
assert os.environ["MODE"] == "dev"
# Cleanup
del os.environ["API_KEY"]
del os.environ["MODE"]


def test_cli_sets_env_file(monkeypatch, tmp_path):
wsgi_server = pretend.stub(run=pretend.call_recorder(lambda *a, **kw: None))
wsgi_app = pretend.stub(run=pretend.call_recorder(lambda *a, **kw: None))
create_app = pretend.call_recorder(lambda *a, **kw: wsgi_app)
monkeypatch.setattr(functions_framework._cli, "create_app", create_app)
create_server = pretend.call_recorder(lambda *a, **kw: wsgi_server)
monkeypatch.setattr(functions_framework._cli, "create_server", create_server)

env_file = tmp_path / ".env"
env_file.write_text("""
# This is a comment
API_KEY=fromfile
MODE=production

# Another comment
FOO=bar
""")

runner = CliRunner()
result = runner.invoke(
_cli,
["--target", "foo", f"--env-file={env_file}"]
)

assert result.exit_code == 0
assert create_app.calls == [pretend.call("foo", None, "http")]
assert wsgi_server.run.calls == [pretend.call("0.0.0.0", 8080)]
assert os.environ["API_KEY"] == "fromfile"
assert os.environ["MODE"] == "production"
assert os.environ["FOO"] == "bar"
# Cleanup
del os.environ["API_KEY"]
del os.environ["MODE"]
del os.environ["FOO"]


def test_invalid_env_format(monkeypatch):
wsgi_server = pretend.stub(run=pretend.call_recorder(lambda *a, **kw: None))
wsgi_app = pretend.stub(run=pretend.call_recorder(lambda *a, **kw: None))
create_app = pretend.call_recorder(lambda *a, **kw: wsgi_app)
monkeypatch.setattr(functions_framework._cli, "create_app", create_app)
create_server = pretend.call_recorder(lambda *a, **kw: wsgi_server)
monkeypatch.setattr(functions_framework._cli, "create_server", create_server)

runner = CliRunner()
result = runner.invoke(
_cli,
["--target", "foo", "--env", "INVALIDENV"]
)

assert result.exit_code != 0
assert "Invalid --env format: 'INVALIDENV'. Expected KEY=VALUE." in result.output


def test_invalid_env_file_line(monkeypatch, tmp_path):
wsgi_server = pretend.stub(run=pretend.call_recorder(lambda *a, **kw: None))
wsgi_app = pretend.stub(run=pretend.call_recorder(lambda *a, **kw: None))
create_app = pretend.call_recorder(lambda *a, **kw: wsgi_app)
monkeypatch.setattr(functions_framework._cli, "create_app", create_app)
create_server = pretend.call_recorder(lambda *a, **kw: wsgi_server)
monkeypatch.setattr(functions_framework._cli, "create_server", create_server)

env_file = tmp_path / ".env"
env_file.write_text("""
API_KEY=fromfile
NOEQUALSIGN
""")

runner = CliRunner()
result = runner.invoke(
_cli,
["--target", "foo", f"--env-file={env_file}"]
)

assert result.exit_code != 0
assert f"Invalid line in env-file '{env_file}': NOEQUALSIGN" in result.output
8 changes: 4 additions & 4 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ deps =
extras =
async
setenv =
PYTESTARGS = --cov=functions_framework --cov-branch --cov-report term-missing --cov-fail-under=100
PYTESTARGS = --cov=functions_framework --cov-branch --cov-report term-missing --cov-fail-under=100 --cov-config=.coveragerc
# Python 3.7: Use .coveragerc-py37 to exclude aio module from coverage since it requires Python 3.8+ (Starlette dependency)
py37-ubuntu-22.04: PYTESTARGS = --cov=functions_framework --cov-config=.coveragerc-py37 --cov-branch --cov-report term-missing --cov-fail-under=100
py37-macos-13: PYTESTARGS = --cov=functions_framework --cov-config=.coveragerc-py37 --cov-branch --cov-report term-missing --cov-fail-under=100
py37-windows-latest: PYTESTARGS =
windows-latest: PYTESTARGS =
py37-windows-latest: PYTESTARGS = --cov=functions_framework --cov-branch --cov-report term-missing --cov-fail-under=100 --cov-config=.coveragerc
windows-latest: PYTESTARGS = --cov=functions_framework --cov-branch --cov-report term-missing --cov-fail-under=100 --cov-config=.coveragerc
commands = pytest {env:PYTESTARGS} {posargs}

[testenv:lint]
Expand All @@ -55,4 +55,4 @@ commands =
isort -c src tests conftest.py
mypy tests/test_typing.py
python -m build
twine check dist/*
twine check dist/*