feat(scripts): provide `diagrams` CLI (#524)

* feat(scripts): provide `diagrams` CLI

This change addresses https://github.com/mingrammer/diagrams/issues/369
while providing a simple CLI entry point which can be used outside the
virtual environment of the installed package.

This works well with for example with [pipx](https://pipxproject.github.io/pipx/)
or [uv](https://docs.astral.sh/uv/).

* feat(scripts): add docstring and tests

* feat(scripts): Fix pre-commit error

* feat(scripts): Fix pre-commit error 2nd try

---------

Co-authored-by: tessier <tessier@luxeys.com>
pull/1143/head
Cedrik Neumann 4 months ago committed by GitHub
parent 42ecd09e02
commit 0a67f0ea91
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,38 @@
import argparse
import sys
def run() -> int:
"""
Run diagrams code files in a diagrams environment.
Args:
paths: A list of paths to Python files containing diagrams code.
Returns:
The exit code.
"""
parser = argparse.ArgumentParser(
description="Run diagrams code files in a diagrams environment.",
)
parser.add_argument(
"paths",
metavar="path",
type=str,
nargs="+",
help="a Python file containing diagrams code",
)
args = parser.parse_args()
for path in args.paths:
with open(path, encoding='utf-8') as f:
exec(f.read())
return 0
def main():
sys.exit(run())
if __name__ == "__main__":
main()

@ -20,6 +20,9 @@ $ pipenv install diagrams
# using poetry
$ poetry add diagrams
# using uv
$ uv tool install diagrams
```
## Quick Start
@ -47,6 +50,14 @@ This generates the diagram below:
It will be saved as `web_service.png` in your working directory.
### CLI
With the `diagrams` CLI you can process one or more diagram files at once.
```shell
$ diagrams diagram1.py diagram2.py
```
## Next
See more [Examples](/docs/getting-started/examples) or see the [Guides](/docs/guides/diagram) page for more details.

@ -9,6 +9,9 @@ homepage = "https://diagrams.mingrammer.com"
repository = "https://github.com/mingrammer/diagrams"
include = ["resources/**/*"]
[tool.poetry.scripts]
diagrams="diagrams.cli:main"
[tool.poetry.dependencies]
python = "^3.9"
graphviz = ">=0.13.2,<0.21.0"

@ -0,0 +1,95 @@
import os
import unittest
from io import StringIO
from unittest.mock import mock_open, patch
from diagrams.cli import run
class CliTest(unittest.TestCase):
def setUp(self):
self.test_file = "test_diagram.py"
# dummy content for the test file
self.test_content_1 = """
from diagrams import Diagram
with Diagram(name="Test", show=False):
pass
"""
# content from getting started examples with utf-8
# only support the installed fonts defined in Dockerfile
self.test_content_2 = """
from diagrams import Diagram
from diagrams.aws.compute import EC2
from diagrams.aws.database import RDS
from diagrams.aws.network import ELB
with Diagram("test_2", show=False, direction="TB"):
ELB("lb") >> [EC2("ワーカー1"),
EC2("작업자 2를"),
EC2("робітник 3"),
EC2("worker4"),
EC2("työntekijä 4")] >> RDS("events")
"""
def tearDown(self):
try:
os.remove("test.png")
except FileNotFoundError:
pass
def test_run_with_valid_file(self):
# write the test file
with open(self.test_file, "w") as f:
f.write(self.test_content_1)
with patch("sys.argv", ["diagrams", self.test_file]):
exit_code = run()
self.assertEqual(exit_code, 0)
try:
os.remove(self.test_file)
except FileNotFoundError:
pass
def test_run_with_multiple_files(self):
multiple_files = ["file1.py", "file2.py"]
# write the code files
with open("file1.py", "w") as f:
f.write(self.test_content_1)
with open("file2.py", "w") as f:
f.write(self.test_content_2)
with patch("sys.argv", ["diagrams"] + multiple_files):
exit_code = run()
self.assertEqual(exit_code, 0)
# cleanup code file
for one_file in multiple_files:
try:
os.remove(one_file)
except FileNotFoundError:
pass
# cleanup generated image
try:
os.remove("test_2.png")
except FileNotFoundError:
pass
def test_run_with_no_arguments(self):
with patch("sys.argv", ["diagrams"]):
with patch("sys.stderr", new=StringIO()) as fake_stderr:
with self.assertRaises(SystemExit):
run()
self.assertIn("the following arguments are required: path", fake_stderr.getvalue())
def test_run_with_nonexistent_file(self):
with patch("sys.argv", ["diagrams", "nonexistent.py"]):
with self.assertRaises(FileNotFoundError):
run()
def test_run_with_invalid_python_code(self):
invalid_content = "this is not valid python code"
with patch("builtins.open", mock_open(read_data=invalid_content)):
with patch("sys.argv", ["diagrams", self.test_file]):
with self.assertRaises(SyntaxError):
run()
Loading…
Cancel
Save