From c210c36cbda8e2bfe1bc364bc3ad52b05cf87d81 Mon Sep 17 00:00:00 2001 From: Keaton Cross Date: Fri, 23 Jun 2023 11:20:50 -0500 Subject: [PATCH 1/3] feat: add support for docs directive --- pystructurizr/cli.py | 21 +++++++++++++++------ pystructurizr/cli_helper.py | 7 +++++-- pystructurizr/dsl.py | 31 ++++++++++++++++++++----------- pystructurizr/generator.py | 7 +++++-- 4 files changed, 45 insertions(+), 21 deletions(-) diff --git a/pystructurizr/cli.py b/pystructurizr/cli.py index 7141571..35eb8c4 100644 --- a/pystructurizr/cli.py +++ b/pystructurizr/cli.py @@ -16,8 +16,11 @@ help='The view file to generate.') @click.option('--as-json', is_flag=True, default=False, help='Dumps the generated code and the imported modules as a json object') -def dump(view, as_json): - diagram_code, imported_modules = generate_diagram_code_in_child_process(view) +@click.option( + "--docs", prompt="Flag to add the !docs directive or omit", help="Flag to add the !docs directive or omit", + is_flag=True, default=True) +def dump(view, as_json, docs: bool): + diagram_code, imported_modules = generate_diagram_code_in_child_process(view, docs) if as_json: print(json.dumps({ "code": diagram_code, @@ -30,7 +33,10 @@ def dump(view, as_json): @click.command() @click.option('--view', prompt='Your view file (e.g. examples.single_file_example)', help='The view file to develop.') -def dev(view): +@click.option( + "--docs", prompt="Flag to add the !docs directive or omit", help="Flag to add the !docs directive or omit", + is_flag=True, default=False) +def dev(view, docs: bool): click.echo(f"Setting up live preview of view {view}...") # Prep the /tmp/pystructurizr folder tmp_folder = ensure_tmp_folder_exists() @@ -40,7 +46,7 @@ def dev(view): async def async_behavior(): print("Generating diagram...") - diagram_code, imported_modules = generate_diagram_code_in_child_process(view) + diagram_code, imported_modules = generate_diagram_code_in_child_process(view, docs) await generate_svg(diagram_code, tmp_folder) return imported_modules @@ -63,10 +69,13 @@ async def observe_loop(): help='The name of the bucket to use on Google Cloud Storage.') @click.option('--object-name', prompt='Name of the object on Google Cloud Storage', help='The name of the object to use on Google Cloud Storage.') -def build(view, gcs_credentials, bucket_name, object_name): +@click.option( + "--docs", prompt="Flag to add the !docs directive or omit", help="Flag to add the !docs directive or omit", + is_flag=True, default=False) +def build(view, gcs_credentials, bucket_name, object_name, docs: bool): async def async_behavior(): # Generate diagram - diagram_code, _ = generate_diagram_code_in_child_process(view) + diagram_code, _ = generate_diagram_code_in_child_process(view, docs) tmp_folder = ensure_tmp_folder_exists() # Generate SVG diff --git a/pystructurizr/cli_helper.py b/pystructurizr/cli_helper.py index 89ffcad..9da904c 100644 --- a/pystructurizr/cli_helper.py +++ b/pystructurizr/cli_helper.py @@ -8,10 +8,13 @@ import httpx -def generate_diagram_code_in_child_process(view: str) -> tuple[dict, list[str]]: +def generate_diagram_code_in_child_process(view: str, docs: bool) -> tuple[dict, list[str]]: def run_child_process(): # Run a separate Python script as a child process - output = subprocess.check_output([sys.executable, "-m", "pystructurizr.generator", "dump", "--view", view]) + if docs: + output = subprocess.check_output([sys.executable, "-m", "pystructurizr.generator", "dump", "--view", view, "--docs"]) + else: + output = subprocess.check_output([sys.executable, "-m", "pystructurizr.generator", "dump", "--view", view]) return output.decode().strip() # Run the child process and capture its output diff --git a/pystructurizr/dsl.py b/pystructurizr/dsl.py index e0f1e1e..bdd3f3d 100644 --- a/pystructurizr/dsl.py +++ b/pystructurizr/dsl.py @@ -74,7 +74,7 @@ def dump_relationships(self, dumper: Dumper) -> None: class Person(Element): - def dump(self, dumper: Dumper) -> None: + def dump(self, dumper: Dumper, with_docs: bool = True) -> None: dumper.add(f'{self.instname} = Person "{self.name}" "{self.description}" {{') dumper.indent() if self.technology: @@ -106,9 +106,10 @@ def dump_relationships(self, dumper: Dumper) -> None: class Container(Element): - def __init__(self, name: str, description: Optional[str]=None, technology: Optional[str]=None, tags: Optional[List[str]]=None): + def __init__(self, name: str, description: Optional[str]=None, technology: Optional[str]=None, tags: Optional[List[str]]=None, docs: str = None): super().__init__(name, description, technology, tags) self.elements = [] + self.docs = docs def __enter__(self): return self @@ -134,9 +135,11 @@ def Group(self, *args, **kwargs) -> "Group": self.elements.append(g) return g - def dump(self, dumper: Dumper) -> None: + def dump(self, dumper: Dumper, with_docs: bool = True) -> None: dumper.add(f'{self.instname} = Container "{self.name}" "{self.description}" {{') dumper.indent() + if with_docs and self.docs: + dumper.add(f"!docs {self.docs}") if self.technology: dumper.add(f'technology "{self.technology}"') if self.tags: @@ -154,9 +157,10 @@ def dump_relationships(self, dumper: Dumper) -> None: class SoftwareSystem(Element): - def __init__(self, name: str, description: Optional[str]=None, technology: Optional[str]=None, tags: Optional[List[str]]=None): + def __init__(self, name: str, description: Optional[str]=None, technology: Optional[str]=None, tags: Optional[List[str]]=None, docs: str = None): super().__init__(name, description, technology, tags) self.elements = [] + self.docs = docs def __enter__(self): return self @@ -182,15 +186,17 @@ def Group(self, *args, **kwargs) -> "Group": self.elements.append(g) return g - def dump(self, dumper: Dumper) -> None: + def dump(self, dumper: Dumper, with_docs: bool = True) -> None: dumper.add(f'{self.instname} = SoftwareSystem "{self.name}" "{self.description}" {{') dumper.indent() + if with_docs and self.docs: + dumper.add(f"!docs {self.docs}") if self.technology: dumper.add(f'technology "{self.technology}"') if self.tags: dumper.add(f'tags "{", ".join(self.tags)}"') for el in self.elements: - el.dump(dumper) + el.dump(dumper, with_docs) dumper.outdent() dumper.add('}') @@ -308,9 +314,9 @@ def Group(self, *args, **kwargs) -> Group: self.elements.append(g) return g - def dump(self, dumper: Dumper) -> None: + def dump(self, dumper: Dumper, with_docs: bool = False) -> None: for element in self.elements: - element.dump(dumper) + element.dump(dumper, with_docs) def dump_relationships(self, dumper: Dumper) -> None: for element in self.elements: @@ -382,7 +388,7 @@ def dump(self, dumper: Dumper) -> None: class Workspace: - def __init__(self): + def __init__(self, docs: str = None): self.models = [] self.views = [] self.styles = [] @@ -416,6 +422,7 @@ def __init__(self): "shape": "Cylinder" } ) + self.docs = docs def __enter__(self): return self @@ -423,9 +430,11 @@ def __enter__(self): def __exit__(self, exc_type, exc_val, exc_tb): pass - def dump(self, dumper: Dumper = Dumper()) -> None: + def dump(self, dumper: Dumper = Dumper(), with_docs=True) -> None: dumper.add('workspace {') dumper.indent() + if with_docs and self.docs: + dumper.add(f"!docs {self.docs}") dumper.add('model {') dumper.indent() @@ -435,7 +444,7 @@ def dump(self, dumper: Dumper = Dumper()) -> None: dumper.outdent() dumper.add('}') for model in self.models: - model.dump(dumper) + model.dump(dumper, with_docs) for model in self.models: model.dump_relationships(dumper) dumper.outdent() diff --git a/pystructurizr/generator.py b/pystructurizr/generator.py index f9e633c..83eb1f8 100644 --- a/pystructurizr/generator.py +++ b/pystructurizr/generator.py @@ -8,12 +8,15 @@ @click.command() @click.option('--view', prompt='Your view file (e.g. example.componentview)', help='The view file to generate.') -def dump(view: str): +@click.option( + "--docs", prompt="Flag to add the !docs directive or omit", help="Flag to add the !docs directive or omit", + is_flag=True, default=False) +def dump(view: str, docs: bool): try: initial_modules = set(sys.modules.keys()) module = importlib.import_module(view) imported_modules = set(sys.modules.keys()) - initial_modules - code = module.workspace.dump() + code = module.workspace.dump(with_docs=docs) print(json.dumps({ "code": code, "imported_modules": list(imported_modules) From 622fc2efa99d02cf38c76a474efe029f2e788df9 Mon Sep 17 00:00:00 2001 From: Keaton Cross Date: Thu, 29 Jun 2023 14:30:48 -0500 Subject: [PATCH 2/3] fix: remove --docs options when using kroki.io --- pystructurizr/cli.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/pystructurizr/cli.py b/pystructurizr/cli.py index 35eb8c4..1ec32d8 100644 --- a/pystructurizr/cli.py +++ b/pystructurizr/cli.py @@ -33,10 +33,7 @@ def dump(view, as_json, docs: bool): @click.command() @click.option('--view', prompt='Your view file (e.g. examples.single_file_example)', help='The view file to develop.') -@click.option( - "--docs", prompt="Flag to add the !docs directive or omit", help="Flag to add the !docs directive or omit", - is_flag=True, default=False) -def dev(view, docs: bool): +def dev(view): click.echo(f"Setting up live preview of view {view}...") # Prep the /tmp/pystructurizr folder tmp_folder = ensure_tmp_folder_exists() @@ -46,7 +43,7 @@ def dev(view, docs: bool): async def async_behavior(): print("Generating diagram...") - diagram_code, imported_modules = generate_diagram_code_in_child_process(view, docs) + diagram_code, imported_modules = generate_diagram_code_in_child_process(view, False) await generate_svg(diagram_code, tmp_folder) return imported_modules @@ -69,13 +66,10 @@ async def observe_loop(): help='The name of the bucket to use on Google Cloud Storage.') @click.option('--object-name', prompt='Name of the object on Google Cloud Storage', help='The name of the object to use on Google Cloud Storage.') -@click.option( - "--docs", prompt="Flag to add the !docs directive or omit", help="Flag to add the !docs directive or omit", - is_flag=True, default=False) -def build(view, gcs_credentials, bucket_name, object_name, docs: bool): +def build(view, gcs_credentials, bucket_name, object_name): async def async_behavior(): # Generate diagram - diagram_code, _ = generate_diagram_code_in_child_process(view, docs) + diagram_code, _ = generate_diagram_code_in_child_process(view, False) tmp_folder = ensure_tmp_folder_exists() # Generate SVG From 87d80dbcd175a7362cd485377bd28adfabf79cb2 Mon Sep 17 00:00:00 2001 From: Keaton Cross Date: Thu, 3 Aug 2023 15:49:51 -0500 Subject: [PATCH 3/3] refactor: change implementation --- pystructurizr/cli.py | 8 ++++---- pystructurizr/cli_helper.py | 10 +++++----- pystructurizr/dsl.py | 37 +++++++++++++++++++++---------------- pystructurizr/generator.py | 9 ++++++--- 4 files changed, 36 insertions(+), 28 deletions(-) diff --git a/pystructurizr/cli.py b/pystructurizr/cli.py index 1ec32d8..acbd103 100644 --- a/pystructurizr/cli.py +++ b/pystructurizr/cli.py @@ -17,10 +17,10 @@ @click.option('--as-json', is_flag=True, default=False, help='Dumps the generated code and the imported modules as a json object') @click.option( - "--docs", prompt="Flag to add the !docs directive or omit", help="Flag to add the !docs directive or omit", - is_flag=True, default=True) -def dump(view, as_json, docs: bool): - diagram_code, imported_modules = generate_diagram_code_in_child_process(view, docs) + "--directives", help="Flag to add extra directives (i.e. !docs) or omit", + is_flag=True, default=False) +def dump(view, as_json, directives: bool): + diagram_code, imported_modules = generate_diagram_code_in_child_process(view, directives) if as_json: print(json.dumps({ "code": diagram_code, diff --git a/pystructurizr/cli_helper.py b/pystructurizr/cli_helper.py index 9da904c..956e552 100644 --- a/pystructurizr/cli_helper.py +++ b/pystructurizr/cli_helper.py @@ -8,13 +8,13 @@ import httpx -def generate_diagram_code_in_child_process(view: str, docs: bool) -> tuple[dict, list[str]]: +def generate_diagram_code_in_child_process(view: str, directives: bool) -> tuple[dict, list[str]]: def run_child_process(): # Run a separate Python script as a child process - if docs: - output = subprocess.check_output([sys.executable, "-m", "pystructurizr.generator", "dump", "--view", view, "--docs"]) - else: - output = subprocess.check_output([sys.executable, "-m", "pystructurizr.generator", "dump", "--view", view]) + executable = [sys.executable, "-m", "pystructurizr.generator", "dump", "--view", view] + if directives: + executable.append("--directives") + output = subprocess.check_output(executable) return output.decode().strip() # Run the child process and capture its output diff --git a/pystructurizr/dsl.py b/pystructurizr/dsl.py index bdd3f3d..0eeb75b 100644 --- a/pystructurizr/dsl.py +++ b/pystructurizr/dsl.py @@ -1,7 +1,7 @@ import keyword import re from enum import Enum -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Literal # pylint: disable=too-few-public-methods @@ -35,10 +35,15 @@ def make_identifier(name: str) -> str: class Dumper: - def __init__(self): + def __init__(self, with_directives: bool = True): + self.with_directives = with_directives self.level = 0 self.lines = [] + def add_directive(self, type: Literal['!docs', '!adrs'], value: str) -> None: + if self.with_directives: + self.add(f"{type} {value}") + def add(self, txt: str) -> None: self.lines.append(f'{" " * self.level}{txt}') @@ -74,7 +79,7 @@ def dump_relationships(self, dumper: Dumper) -> None: class Person(Element): - def dump(self, dumper: Dumper, with_docs: bool = True) -> None: + def dump(self, dumper: Dumper) -> None: dumper.add(f'{self.instname} = Person "{self.name}" "{self.description}" {{') dumper.indent() if self.technology: @@ -135,11 +140,11 @@ def Group(self, *args, **kwargs) -> "Group": self.elements.append(g) return g - def dump(self, dumper: Dumper, with_docs: bool = True) -> None: + def dump(self, dumper: Dumper) -> None: dumper.add(f'{self.instname} = Container "{self.name}" "{self.description}" {{') dumper.indent() - if with_docs and self.docs: - dumper.add(f"!docs {self.docs}") + if self.docs: + dumper.add_directive("!docs", f"{self.docs}") if self.technology: dumper.add(f'technology "{self.technology}"') if self.tags: @@ -186,17 +191,17 @@ def Group(self, *args, **kwargs) -> "Group": self.elements.append(g) return g - def dump(self, dumper: Dumper, with_docs: bool = True) -> None: + def dump(self, dumper: Dumper) -> None: dumper.add(f'{self.instname} = SoftwareSystem "{self.name}" "{self.description}" {{') dumper.indent() - if with_docs and self.docs: - dumper.add(f"!docs {self.docs}") + if self.docs: + dumper.add_directive("!docs", f"{self.docs}") if self.technology: dumper.add(f'technology "{self.technology}"') if self.tags: dumper.add(f'tags "{", ".join(self.tags)}"') for el in self.elements: - el.dump(dumper, with_docs) + el.dump(dumper) dumper.outdent() dumper.add('}') @@ -314,9 +319,9 @@ def Group(self, *args, **kwargs) -> Group: self.elements.append(g) return g - def dump(self, dumper: Dumper, with_docs: bool = False) -> None: + def dump(self, dumper: Dumper) -> None: for element in self.elements: - element.dump(dumper, with_docs) + element.dump(dumper) def dump_relationships(self, dumper: Dumper) -> None: for element in self.elements: @@ -430,11 +435,11 @@ def __enter__(self): def __exit__(self, exc_type, exc_val, exc_tb): pass - def dump(self, dumper: Dumper = Dumper(), with_docs=True) -> None: + def dump(self, dumper: Dumper = Dumper()) -> str: dumper.add('workspace {') dumper.indent() - if with_docs and self.docs: - dumper.add(f"!docs {self.docs}") + if self.docs: + dumper.add_directive("!docs", f"{self.docs}") dumper.add('model {') dumper.indent() @@ -444,7 +449,7 @@ def dump(self, dumper: Dumper = Dumper(), with_docs=True) -> None: dumper.outdent() dumper.add('}') for model in self.models: - model.dump(dumper, with_docs) + model.dump(dumper) for model in self.models: model.dump_relationships(dumper) dumper.outdent() diff --git a/pystructurizr/generator.py b/pystructurizr/generator.py index 83eb1f8..e908c50 100644 --- a/pystructurizr/generator.py +++ b/pystructurizr/generator.py @@ -4,19 +4,22 @@ import click +from pystructurizr.dsl import Dumper + @click.command() @click.option('--view', prompt='Your view file (e.g. example.componentview)', help='The view file to generate.') @click.option( - "--docs", prompt="Flag to add the !docs directive or omit", help="Flag to add the !docs directive or omit", + "--directives", help="Flag to add extra directives (i.e. !docs) or omit", is_flag=True, default=False) -def dump(view: str, docs: bool): +def dump(view: str, directives: bool): try: initial_modules = set(sys.modules.keys()) module = importlib.import_module(view) imported_modules = set(sys.modules.keys()) - initial_modules - code = module.workspace.dump(with_docs=docs) + dumper = Dumper(with_directives=directives) + code = module.workspace.dump(dumper=dumper) print(json.dumps({ "code": code, "imported_modules": list(imported_modules)