From f44a1797f8a9dfc5422f7afb7bb61045576eaba3 Mon Sep 17 00:00:00 2001 From: Niels Vanspauwen Date: Sat, 17 Jun 2023 22:50:29 +0200 Subject: [PATCH] lint --- .pylintrc | 5 +++ pystructurizr/cli.py | 17 +++++--- pystructurizr/cli_helper.py | 17 ++++---- pystructurizr/cli_watcher.py | 5 ++- pystructurizr/cloudstorage.py | 28 ++++++------ pystructurizr/dsl.py | 82 ++++++++++++++++++----------------- test/test_cli.py | 9 ++-- 7 files changed, 90 insertions(+), 73 deletions(-) create mode 100644 .pylintrc diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..323d47a --- /dev/null +++ b/.pylintrc @@ -0,0 +1,5 @@ +[pycodestyle] +max-line-length = 140 + +[pylint.messages control] +disable = missing-module-docstring, missing-function-docstring, missing-class-docstring diff --git a/pystructurizr/cli.py b/pystructurizr/cli.py index a7a096e..d24eefd 100644 --- a/pystructurizr/cli.py +++ b/pystructurizr/cli.py @@ -1,19 +1,21 @@ -import click import asyncio +import json import os import shutil import subprocess -import json -from .cli_helper import generate_diagram_code, generate_diagram_code_in_child_process, generate_svg, ensure_tmp_folder_exists -from .cli_watcher import observe_modules -from .cloudstorage import create_cloud_storage, CloudStorage +import click + +from .cli_helper import (ensure_tmp_folder_exists, generate_diagram_code, + generate_diagram_code_in_child_process, generate_svg) +from .cli_watcher import observe_modules +from .cloudstorage import CloudStorage, create_cloud_storage @click.command() @click.option('--view', prompt='Your view file (e.g. example.componentview)', help='The view file to generate.') -@click.option('--as-json', is_flag=True, default=False, +@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(view) @@ -47,6 +49,7 @@ async def async_behavior(): async def observe_loop(): modules_to_watch = await async_behavior() click.echo("Launching webserver...") + # pylint: disable=consider-using-with subprocess.Popen(f"httpwatcher --root {tmp_folder} --watch {tmp_folder}", shell=True) await observe_modules(modules_to_watch, async_behavior) @@ -76,7 +79,7 @@ async def async_behavior(): cloud_storage = create_cloud_storage(CloudStorage.Provider.GCS, gcs_credentials) svg_file_url = cloud_storage.upload_file(svg_file_path, bucket_name, object_name) print(svg_file_url) - + asyncio.run(async_behavior()) diff --git a/pystructurizr/cli_helper.py b/pystructurizr/cli_helper.py index e08d2e7..c7b5217 100644 --- a/pystructurizr/cli_helper.py +++ b/pystructurizr/cli_helper.py @@ -1,13 +1,12 @@ -import click import importlib +import json +import os import subprocess import sys + import aiofiles import click import httpx -import sys -import os -import json def generate_diagram_code(view: str) -> str: @@ -18,8 +17,10 @@ def generate_diagram_code(view: str) -> str: code = module.workspace.dump() return code, imported_modules except ModuleNotFoundError: + # pylint: disable=raise-missing-from raise click.BadParameter("Invalid view name. Make sure you don't include the .py file extension.") except AttributeError: + # pylint: disable=raise-missing-from raise click.BadParameter("Non-compliant view file: make sure it exports the PyStructurizr workspace.") @@ -35,7 +36,7 @@ def run_child_process(): async def generate_svg(diagram_code: str, tmp_folder: str) -> str: - url = f"https://kroki.io/structurizr/svg" + url = "https://kroki.io/structurizr/svg" async with httpx.AsyncClient() as client: resp = await client.post(url, data=diagram_code) @@ -44,10 +45,10 @@ async def generate_svg(diagram_code: str, tmp_folder: str) -> str: if resp.content: print(resp.content.decode()) raise click.ClickException("Failed to create diagram") - + svg_file_path = f"{tmp_folder}/diagram.svg" - async with aiofiles.open(svg_file_path, "w") as f: - await f.write(resp.text) + async with aiofiles.open(svg_file_path, "w") as svg_file: + await svg_file.write(resp.text) return svg_file_path diff --git a/pystructurizr/cli_watcher.py b/pystructurizr/cli_watcher.py index 45a2630..b71bb18 100644 --- a/pystructurizr/cli_watcher.py +++ b/pystructurizr/cli_watcher.py @@ -1,8 +1,9 @@ +import datetime import os import time -from watchdog.observers import Observer + from watchdog.events import FileSystemEventHandler -import datetime +from watchdog.observers import Observer def formatted_timestamp(): diff --git a/pystructurizr/cloudstorage.py b/pystructurizr/cloudstorage.py index 786eb01..77321b4 100644 --- a/pystructurizr/cloudstorage.py +++ b/pystructurizr/cloudstorage.py @@ -1,9 +1,14 @@ +import json from abc import ABC, abstractmethod from enum import Enum -import json from typing import Dict +import boto3 +from google.cloud import storage +from google.cloud.exceptions import GoogleCloudError + +# pylint: disable=too-few-public-methods class CloudStorage(ABC): class Provider(Enum): GCS = "GCS" @@ -12,29 +17,27 @@ class Provider(Enum): @abstractmethod def upload_file(self, file_path: str, bucket_name: str, object_name: str) -> str: pass - + class GCS(CloudStorage): def __init__(self, gcs_credentials): self.gcs_credentials = gcs_credentials def upload_file(self, file_path: str, bucket_name: str, object_name: str) -> str: - from google.cloud import storage - from google.cloud.exceptions import GoogleCloudError try: gcs_client = storage.Client.from_service_account_json(self.gcs_credentials) gcs_bucket = gcs_client.get_bucket(bucket_name) gcs_blob = gcs_bucket.blob(object_name) gcs_blob.upload_from_filename(file_path) return f"https://storage.googleapis.com/{bucket_name}/{object_name}" - except GoogleCloudError as e: + except GoogleCloudError as exc: print("An error occurred while uploading the file to Google Cloud Storage:") - print(e) + print(exc) + return "" class S3(CloudStorage): def __init__(self, credentials_file: str): - import boto3 self.credentials = self._load_credentials(credentials_file) self.client = boto3.client( "s3", @@ -42,10 +45,10 @@ def __init__(self, credentials_file: str): aws_secret_access_key=self.credentials["secret_key"], region_name=self.credentials["region"] ) - + def _load_credentials(self, credentials_file: str) -> Dict[str, str]: - with open(credentials_file, "r") as f: - credentials = json.load(f) + with open(credentials_file, "r", encoding='utf-8') as cred: + credentials = json.load(cred) required_keys = ["access_key", "secret_key", "region"] if not all(key in credentials for key in required_keys): raise ValueError("Invalid credentials format") @@ -60,7 +63,6 @@ def upload_file(self, file_path: str, bucket_name: str, object_name: str) -> str def create_cloud_storage(provider: CloudStorage.Provider, credentials_file: str) -> CloudStorage: if provider == CloudStorage.Provider.GCS: return GCS(credentials_file) - elif provider == CloudStorage.Provider.S3: + if provider == CloudStorage.Provider.S3: return S3(credentials_file) - else: - raise ValueError("Invalid cloud storage provider") + raise ValueError("Invalid cloud storage provider") diff --git a/pystructurizr/dsl.py b/pystructurizr/dsl.py index cb09253..5a47b83 100644 --- a/pystructurizr/dsl.py +++ b/pystructurizr/dsl.py @@ -1,9 +1,10 @@ -import re import keyword +import re from enum import Enum from typing import List, Optional +# pylint: disable=too-few-public-methods class Identifier: counter = {} @@ -29,25 +30,23 @@ def make_identifier(name: str) -> str: identifier = f"{identifier}_{Identifier.counter[identifier]}" else: Identifier.counter[identifier] = 1 - + return identifier - + class Dumper: def __init__(self): self.level = 0 self.lines = [] - def add(self, s: str) -> None: - self.lines.append(f'{" " * self.level}{s}') + def add(self, txt: str) -> None: + self.lines.append(f'{" " * self.level}{txt}') def indent(self) -> None: self.level += 1 def outdent(self): - self.level -= 1 - if self.level < 0: - self.level = 0 + self.level = max(self.level - 1, 0) def result(self) -> str: return "\n".join(self.lines) @@ -66,7 +65,7 @@ def uses(self, destination: str, description: Optional[str]=None, technology: Op relationship = Relationship(self, destination, description, technology) self.relationships.append(relationship) return relationship - + def dump(self, dumper: Dumper) -> None: raise NotImplementedError("This method must be implemented in a subclass.") @@ -75,9 +74,6 @@ def dump_relationships(self, dumper: Dumper) -> None: class Person(Element): - def __init__(self, name: str, description: Optional[str]=None, technology: Optional[str]=None, tags: Optional[List[str]]=None): - super().__init__(name, description, technology, tags) - def dump(self, dumper: Dumper) -> None: dumper.add(f'{self.instname} = Person "{self.name}" "{self.description}" {{') dumper.indent() @@ -86,7 +82,7 @@ def dump(self, dumper: Dumper) -> None: if self.tags: dumper.add(f'tags "{", ".join(self.tags)}"') dumper.outdent() - dumper.add(f'}}') + dumper.add('}') def dump_relationships(self, dumper: Dumper) -> None: for rel in self.relationships: @@ -94,9 +90,6 @@ def dump_relationships(self, dumper: Dumper) -> None: class Component(Element): - def __init__(self, name: str, description: Optional[str]=None, technology: Optional[str]=None, tags: Optional[List[str]]=None): - super().__init__(name, description, technology, tags) - def dump(self, dumper: Dumper) -> None: dumper.add(f'{self.instname} = Component "{self.name}" "{self.description}" {{') dumper.indent() @@ -105,7 +98,7 @@ def dump(self, dumper: Dumper) -> None: if self.tags: dumper.add(f'tags "{", ".join(self.tags)}"') dumper.outdent() - dumper.add(f'}}') + dumper.add('}') def dump_relationships(self, dumper: Dumper) -> None: for rel in self.relationships: @@ -117,6 +110,7 @@ def __init__(self, name: str, description: Optional[str]=None, technology: Optio super().__init__(name, description, technology, tags) self.components = [] + # pylint: disable=invalid-name def Component(self, *args, **kwargs) -> Component: if args and isinstance(args[0], Component): component = args[0] @@ -135,7 +129,7 @@ def dump(self, dumper: Dumper) -> None: for component in self.components: component.dump(dumper) dumper.outdent() - dumper.add(f'}}') + dumper.add('}') def dump_relationships(self, dumper: Dumper) -> None: for rel in self.relationships: @@ -149,6 +143,7 @@ def __init__(self, name: str, description: Optional[str]=None, technology: Optio super().__init__(name, description, technology, tags) self.containers = [] + # pylint: disable=invalid-name def Container(self, *args, **kwargs) -> Container: if args and isinstance(args[0], Container): container = args[0] @@ -167,7 +162,7 @@ def dump(self, dumper: Dumper) -> None: for container in self.containers: container.dump(dumper) dumper.outdent() - dumper.add(f'}}') + dumper.add('}') def dump_relationships(self, dumper: Dumper) -> None: for rel in self.relationships: @@ -181,6 +176,7 @@ def __init__(self, name: str): super().__init__(name) self.elements = [] + # pylint: disable=invalid-name def Person(self, *args, **kwargs) -> Person: if args and isinstance(args[0], Person): person = args[0] @@ -189,6 +185,7 @@ def Person(self, *args, **kwargs) -> Person: self.elements.append(person) return person + # pylint: disable=invalid-name def SoftwareSystem(self, *args, **kwargs) -> SoftwareSystem: if args and isinstance(args[0], SoftwareSystem): system = args[0] @@ -210,7 +207,7 @@ class Relationship: def __init__(self, source: Element, destination: Element, description: Optional[str]=None, technology: Optional[str]=None): self.source = source self.destination = destination - self.description = description if description else "" + self.description = description if description else "" self.technology = technology if technology else "" def dump(self, dumper: Dumper) -> None: @@ -235,7 +232,7 @@ def __init__(self, viewkind: Kind, element: Element, name: str, description: Opt def include(self, element: Element) -> 'View': self.includes.append(element) return self - + def exclude(self, element: Element) -> 'View': self.excludes.append(element) return self @@ -252,22 +249,22 @@ def dump(self, dumper: Dumper) -> None: dumper.add(f'exclude {exclude.instname}') dumper.add('autoLayout') dumper.outdent() - dumper.add(f'}}') + dumper.add('}') class Style: - def __init__(self, map: dict[str, str]): - self.map = map + def __init__(self, style_map: dict[str, str]): + self.map = style_map def dump(self, dumper: Dumper) -> None: dumper.add(f'element "{self.map["tag"]}" {{') dumper.indent() - for k, v in self.map.items(): - if k == "tag": + for key, value in self.map.items(): + if key == "tag": continue - dumper.add(f'{k} "{v}"') + dumper.add(f'{key} "{value}"') dumper.outdent() - dumper.add(f'}}') + dumper.add('}') class Workspace: @@ -307,62 +304,67 @@ def __init__(self): ) def dump(self, dumper: Dumper = Dumper()) -> None: - dumper.add(f'workspace {{') + dumper.add('workspace {') dumper.indent() - dumper.add(f'model {{') + dumper.add('model {') dumper.indent() for model in self.models: model.dump(dumper) for model in self.models: model.dump_relationships(dumper) dumper.outdent() - dumper.add(f'}}') + dumper.add('}') - dumper.add(f'views {{') + dumper.add('views {') dumper.indent() for view in self.views: view.dump(dumper) - dumper.add(f'styles {{') + dumper.add('styles {') dumper.indent() for style in self.styles: style.dump(dumper) dumper.outdent() - dumper.add(f'}}') + dumper.add('}') dumper.outdent() - dumper.add(f'}}') + dumper.add('}') dumper.outdent() - dumper.add(f'}}') + dumper.add('}') return dumper.result() + # pylint: disable=invalid-name def Model(self, model: Optional[Model]=None, name: Optional[str]=None): if model is None: model = Model(name) self.models.append(model) return model + # pylint: disable=invalid-name def SystemLandscapeView(self, name: str, description: str): view = View(View.Kind.SYSTEM_LANDSCAPE, None, name, description) self.views.append(view) return view - + + # pylint: disable=invalid-name def SystemContextView(self, element: Element, name: str, description: str): view = View(View.Kind.SYSTEM_CONTEXT, element, name, description) self.views.append(view) return view - + + # pylint: disable=invalid-name def ContainerView(self, element: Element, name: str, description: str): view = View(View.Kind.CONTAINER, element, name, description) self.views.append(view) return view - + + # pylint: disable=invalid-name def ComponentView(self, element: Element, name: str, description: str): view = View(View.Kind.COMPONENT, element, name, description) self.views.append(view) return view + # pylint: disable=invalid-name def Styles(self, *styles: dict[str, str]) -> None: for style in styles: self.styles.append(Style(style)) - \ No newline at end of file diff --git a/test/test_cli.py b/test/test_cli.py index 2e9458c..d42da6a 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -1,8 +1,11 @@ +import os import unittest + from click.testing import CliRunner -import os + from pystructurizr import cli + class CliTest(unittest.TestCase): def setUp(self): self.runner = CliRunner() @@ -14,8 +17,8 @@ def test_dump(self): result = self.runner.invoke(cli.dump, ['--view', 'example.systemlandscapeview']) gold_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'example.gold') - with open(gold_file) as f: - gold = f.read() + with open(gold_file, "r", encoding='utf-8') as gfile: + gold = gfile.read() self.assertEqual(result.exit_code, 0) self.assertEqual(result.output.strip(), gold)