diff --git a/CHANGELOG.md b/CHANGELOG.md index fb4f286..eb50c9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -## [0.5.0] - 2023-01-29 +## [0.6.0] - 2023-02-16 -* Dependencies rules: Allow multiple importer packages +### Added + +* "Expensive Loop" rule template and command + +## [0.5.0] - 2023-01-29 ### Added +* Dependencies rules: Allow multiple importer packages + ## [0.4.0] - 2023-01-05 ### Added diff --git a/README.md b/README.md index 891b3a7..84fe9eb 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Supported templates: * [dependencies](#create-dependencies-rules) * [naming / voldemort](#create-voldemort-rules): avoid some names * naming / name vs type mismatch (coming soon) +* performance / expensive loop For example: @@ -133,6 +134,14 @@ You'll be prompted to provide: * variable declarations * variable assignments +## Expensive Loop + +Loops often cause performance problems. Especially, if they execute expensive operations: talking to external systems, complex calculations. + +``` +sourcery-rules expensive-loop create +``` + ## Using the Generated Rules The generated rules can be used by Sourcery to review your project. @@ -142,4 +151,15 @@ All the generated rules have the tag `architecture`. Once you've copied them to ``` sourcery review --enable architecture . -``` \ No newline at end of file +``` + +You'll be prompted to provide: + +* the fully qualified name of the function that shouldn't be called in loops + +=> + +2 rules will be generated: + +* for `for` loops +* for `while` loops diff --git a/pyproject.toml b/pyproject.toml index 2d61674..f2d08e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "sourcery-rules-generator" -version = "0.5.0" +version = "0.6.0" description = "Generate architecture rules for Python projects." license = "MIT" authors = ["reka "] diff --git a/sourcery_rules_generator/__init__.py b/sourcery_rules_generator/__init__.py index 3d18726..906d362 100644 --- a/sourcery_rules_generator/__init__.py +++ b/sourcery_rules_generator/__init__.py @@ -1 +1 @@ -__version__ = "0.5.0" +__version__ = "0.6.0" diff --git a/sourcery_rules_generator/cli/cli.py b/sourcery_rules_generator/cli/cli.py index 27d20e2..186f4a4 100755 --- a/sourcery_rules_generator/cli/cli.py +++ b/sourcery_rules_generator/cli/cli.py @@ -4,12 +4,17 @@ from rich.console import Console -from sourcery_rules_generator.cli import dependencies_cli, voldemort_cli +from sourcery_rules_generator.cli import ( + dependencies_cli, + voldemort_cli, + expensive_loop_cli, +) from sourcery_rules_generator import __version__ app = typer.Typer(rich_markup_mode="markdown") app.add_typer(dependencies_cli.app, name="dependencies") app.add_typer(voldemort_cli.app, name="voldemort") +app.add_typer(expensive_loop_cli.app, name="expensive-loop") @app.callback(invoke_without_command=True) diff --git a/sourcery_rules_generator/cli/expensive_loop_cli.py b/sourcery_rules_generator/cli/expensive_loop_cli.py new file mode 100755 index 0000000..c9e5760 --- /dev/null +++ b/sourcery_rules_generator/cli/expensive_loop_cli.py @@ -0,0 +1,89 @@ +#! /usr/bin/env python3 + +import sys +import typer +from rich.console import Console +from rich.markdown import Markdown +from rich.prompt import Prompt +from rich.syntax import Syntax + +from sourcery_rules_generator import expensive_loop + +app = typer.Typer(rich_markup_mode="markdown") + + +DESCRIPTION_MARKDOWN = """ +# Expensive Loop Template + +With the "Expensive Loop" template, +you can generate rules that ensure that an expensive operation isn't called in a loop. + +For example: + +* Call to an external API. +* Complex calculations. + +## Parameters for the "Expensive Loop" Template + +1. The fully qualified name of the expensive function, that you want to avoid in loops. Required. +""" + + +@app.command() +def create( + avoided_function_option: str = typer.Option( + None, + "--avoided-function", + help="The function that should be avoided in loops.", + ), + interactive_flag: bool = typer.Option( + True, + "--interactive/--no-interactive", + "--input/--no-input", + help="Switch whether interactive prompts are shown. Use `--no-input` when you call this command from scripts.", + ), + plain: bool = typer.Option(False, help="Print only plain text."), + quiet: bool = typer.Option( + False, + "--quiet", + "-q", + help='Display less info about the "Expensive Loop" template.', + ), +): + """Create a new Sourcery Expensive Loop rule.""" + interactive = sys.stdin.isatty() and interactive_flag + stderr_console = Console(stderr=True) + if interactive and not quiet: + stderr_console.print(Markdown(DESCRIPTION_MARKDOWN)) + stderr_console.rule() + + if interactive: + stderr_console.print( + Markdown('## Parameters for the "Expensive Loop" Template') + ) + + function_name = ( + avoided_function_option + or interactive + and Prompt.ask( + "The fully qualified name of the expensive function (required)", + console=stderr_console, + ) + ) + if not function_name: + _raise_error("No function name provided. Can't create Expensive Loop rule.") + + result = expensive_loop.create_yaml_rules(function_name) + + stderr_console.rule() + stderr_console.print(Markdown("## Generated YAML Rules")) + if plain: + Console().print(result) + else: + Console().print(Syntax(result, "YAML")) + + +def _raise_error(error_msg: str, code: int = 1) -> None: + stderr_console = Console(stderr=True, style="bold red") + stderr_console.print(error_msg) + raise typer.Exit(code=code) diff --git a/sourcery_rules_generator/expensive_loop.py b/sourcery_rules_generator/expensive_loop.py new file mode 100644 index 0000000..f192bd7 --- /dev/null +++ b/sourcery_rules_generator/expensive_loop.py @@ -0,0 +1,44 @@ +from sourcery_rules_generator import yaml_converter +from sourcery_rules_generator.models import SourceryCustomRule + + +def create_yaml_rules(function_name: str): + + custom_rules = create_sourcery_custom_rules(function_name) + + rules_dict = {"rules": [rule.dict(exclude_unset=True) for rule in custom_rules]} + return yaml_converter.dumps(rules_dict) + + +def create_sourcery_custom_rules(function_name: str) -> str: + description = f"Don't call `{function_name}()` in loops." + function_slug = function_name.replace(".", "-") + tag = f"no-{function_slug}-in-loops" + + for_rule = SourceryCustomRule( + id=f"no-{function_slug}-for", + description=description, + tags=["performance", tag], + pattern=f""" +for ... in ... : + ... + {function_name}(...) + ... +""", + ) + while_rule = SourceryCustomRule( + id=f"no-{function_slug}-while", + description=description, + tags=["performance", tag], + pattern=f""" +while ... : + ... + {function_name}(...) + ... +""", + ) + + return ( + for_rule, + while_rule, + ) diff --git a/sourcery_rules_generator/voldemort.py b/sourcery_rules_generator/voldemort.py index 4086f94..07aaa9a 100644 --- a/sourcery_rules_generator/voldemort.py +++ b/sourcery_rules_generator/voldemort.py @@ -1,7 +1,5 @@ -from typing import Optional - from sourcery_rules_generator import yaml_converter -from sourcery_rules_generator.models import SourceryCustomRule, PathsConfig +from sourcery_rules_generator.models import SourceryCustomRule def create_yaml_rules(name_to_avoid: str): diff --git a/tests/test_version.py b/tests/test_version.py index a777456..a00e970 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -2,4 +2,4 @@ def test_version(): - assert __version__ == "0.5.0" + assert __version__ == "0.6.0" diff --git a/tests/unit/test_expensive_loop.py b/tests/unit/test_expensive_loop.py new file mode 100644 index 0000000..7b42891 --- /dev/null +++ b/tests/unit/test_expensive_loop.py @@ -0,0 +1,33 @@ +from sourcery_rules_generator import expensive_loop +from sourcery_rules_generator.models import SourceryCustomRule + + +def test_fully_qualified_function_name(): + result = expensive_loop.create_sourcery_custom_rules("custom_lib.api.create_item") + + expected = ( + SourceryCustomRule( + id="no-custom_lib-api-create_item-for", + description="Don't call `custom_lib.api.create_item()` in loops.", + tags=["performance", "no-custom_lib-api-create_item-in-loops"], + pattern=""" +for ... in ... : + ... + custom_lib.api.create_item(...) + ... +""", + ), + SourceryCustomRule( + id="no-custom_lib-api-create_item-while", + description="Don't call `custom_lib.api.create_item()` in loops.", + tags=["performance", "no-custom_lib-api-create_item-in-loops"], + pattern=""" +while ... : + ... + custom_lib.api.create_item(...) + ... +""", + ), + ) + + assert result == expected diff --git a/tests/unit/test_voldemort.py b/tests/unit/test_voldemort.py index 755a3f9..3544aa1 100644 --- a/tests/unit/test_voldemort.py +++ b/tests/unit/test_voldemort.py @@ -2,7 +2,7 @@ from sourcery_rules_generator.models import SourceryCustomRule -def test_1_allowed_importer(): +def test_do_not_allow_util(): result = voldemort.create_sourcery_custom_rules("util") expected = (