Skip to content

Commit 588a62d

Browse files
authored
feat: Expensive Loop rule template (#17)
* feat: Expensive Loop rule template Avoid expensive function calls in loops. Generated rules: * `for` * `while`
1 parent 3c6f561 commit 588a62d

File tree

11 files changed

+206
-11
lines changed

11 files changed

+206
-11
lines changed

CHANGELOG.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10-
## [0.5.0] - 2023-01-29
10+
## [0.6.0] - 2023-02-16
1111

12-
* Dependencies rules: Allow multiple importer packages
12+
### Added
13+
14+
* "Expensive Loop" rule template and command
15+
16+
## [0.5.0] - 2023-01-29
1317

1418
### Added
1519

20+
* Dependencies rules: Allow multiple importer packages
21+
1622
## [0.4.0] - 2023-01-05
1723

1824
### Added

README.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Supported templates:
2121
* [dependencies](#create-dependencies-rules)
2222
* [naming / voldemort](#create-voldemort-rules): avoid some names
2323
* naming / name vs type mismatch (coming soon)
24+
* performance / expensive loop
2425

2526
For example:
2627

@@ -133,6 +134,14 @@ You'll be prompted to provide:
133134
* variable declarations
134135
* variable assignments
135136

137+
## Expensive Loop
138+
139+
Loops often cause performance problems. Especially, if they execute expensive operations: talking to external systems, complex calculations.
140+
141+
```
142+
sourcery-rules expensive-loop create
143+
```
144+
136145
## Using the Generated Rules
137146

138147
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
142151

143152
```
144153
sourcery review --enable architecture .
145-
```
154+
```
155+
156+
You'll be prompted to provide:
157+
158+
* the fully qualified name of the function that shouldn't be called in loops
159+
160+
=>
161+
162+
2 rules will be generated:
163+
164+
* for `for` loops
165+
* for `while` loops

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "sourcery-rules-generator"
3-
version = "0.5.0"
3+
version = "0.6.0"
44
description = "Generate architecture rules for Python projects."
55
license = "MIT"
66
authors = ["reka <reka@sourcery.ai>"]
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.5.0"
1+
__version__ = "0.6.0"

sourcery_rules_generator/cli/cli.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,17 @@
44

55
from rich.console import Console
66

7-
from sourcery_rules_generator.cli import dependencies_cli, voldemort_cli
7+
from sourcery_rules_generator.cli import (
8+
dependencies_cli,
9+
voldemort_cli,
10+
expensive_loop_cli,
11+
)
812
from sourcery_rules_generator import __version__
913

1014
app = typer.Typer(rich_markup_mode="markdown")
1115
app.add_typer(dependencies_cli.app, name="dependencies")
1216
app.add_typer(voldemort_cli.app, name="voldemort")
17+
app.add_typer(expensive_loop_cli.app, name="expensive-loop")
1318

1419

1520
@app.callback(invoke_without_command=True)
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
#! /usr/bin/env python3
2+
3+
import sys
4+
import typer
5+
from rich.console import Console
6+
from rich.markdown import Markdown
7+
from rich.prompt import Prompt
8+
from rich.syntax import Syntax
9+
10+
from sourcery_rules_generator import expensive_loop
11+
12+
app = typer.Typer(rich_markup_mode="markdown")
13+
14+
15+
DESCRIPTION_MARKDOWN = """
16+
# Expensive Loop Template
17+
18+
With the "Expensive Loop" template,
19+
you can generate rules that ensure that an expensive operation isn't called in a loop.
20+
21+
For example:
22+
23+
* Call to an external API.
24+
* Complex calculations.
25+
26+
## Parameters for the "Expensive Loop" Template
27+
28+
1. The fully qualified name of the expensive function, that you want to avoid in loops. Required.
29+
"""
30+
31+
32+
@app.command()
33+
def create(
34+
avoided_function_option: str = typer.Option(
35+
None,
36+
"--avoided-function",
37+
help="The function that should be avoided in loops.",
38+
),
39+
interactive_flag: bool = typer.Option(
40+
True,
41+
"--interactive/--no-interactive",
42+
"--input/--no-input",
43+
help="Switch whether interactive prompts are shown. Use `--no-input` when you call this command from scripts.",
44+
),
45+
plain: bool = typer.Option(False, help="Print only plain text."),
46+
quiet: bool = typer.Option(
47+
False,
48+
"--quiet",
49+
"-q",
50+
help='Display less info about the "Expensive Loop" template.',
51+
),
52+
):
53+
"""Create a new Sourcery Expensive Loop rule."""
54+
interactive = sys.stdin.isatty() and interactive_flag
55+
stderr_console = Console(stderr=True)
56+
if interactive and not quiet:
57+
stderr_console.print(Markdown(DESCRIPTION_MARKDOWN))
58+
stderr_console.rule()
59+
60+
if interactive:
61+
stderr_console.print(
62+
Markdown('## Parameters for the "Expensive Loop" Template')
63+
)
64+
65+
function_name = (
66+
avoided_function_option
67+
or interactive
68+
and Prompt.ask(
69+
"The fully qualified name of the expensive function (required)",
70+
console=stderr_console,
71+
)
72+
)
73+
if not function_name:
74+
_raise_error("No function name provided. Can't create Expensive Loop rule.")
75+
76+
result = expensive_loop.create_yaml_rules(function_name)
77+
78+
stderr_console.rule()
79+
stderr_console.print(Markdown("## Generated YAML Rules"))
80+
if plain:
81+
Console().print(result)
82+
else:
83+
Console().print(Syntax(result, "YAML"))
84+
85+
86+
def _raise_error(error_msg: str, code: int = 1) -> None:
87+
stderr_console = Console(stderr=True, style="bold red")
88+
stderr_console.print(error_msg)
89+
raise typer.Exit(code=code)
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from sourcery_rules_generator import yaml_converter
2+
from sourcery_rules_generator.models import SourceryCustomRule
3+
4+
5+
def create_yaml_rules(function_name: str):
6+
7+
custom_rules = create_sourcery_custom_rules(function_name)
8+
9+
rules_dict = {"rules": [rule.dict(exclude_unset=True) for rule in custom_rules]}
10+
return yaml_converter.dumps(rules_dict)
11+
12+
13+
def create_sourcery_custom_rules(function_name: str) -> str:
14+
description = f"Don't call `{function_name}()` in loops."
15+
function_slug = function_name.replace(".", "-")
16+
tag = f"no-{function_slug}-in-loops"
17+
18+
for_rule = SourceryCustomRule(
19+
id=f"no-{function_slug}-for",
20+
description=description,
21+
tags=["performance", tag],
22+
pattern=f"""
23+
for ... in ... :
24+
...
25+
{function_name}(...)
26+
...
27+
""",
28+
)
29+
while_rule = SourceryCustomRule(
30+
id=f"no-{function_slug}-while",
31+
description=description,
32+
tags=["performance", tag],
33+
pattern=f"""
34+
while ... :
35+
...
36+
{function_name}(...)
37+
...
38+
""",
39+
)
40+
41+
return (
42+
for_rule,
43+
while_rule,
44+
)

sourcery_rules_generator/voldemort.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
from typing import Optional
2-
31
from sourcery_rules_generator import yaml_converter
4-
from sourcery_rules_generator.models import SourceryCustomRule, PathsConfig
2+
from sourcery_rules_generator.models import SourceryCustomRule
53

64

75
def create_yaml_rules(name_to_avoid: str):

tests/test_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22

33

44
def test_version():
5-
assert __version__ == "0.5.0"
5+
assert __version__ == "0.6.0"

tests/unit/test_expensive_loop.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from sourcery_rules_generator import expensive_loop
2+
from sourcery_rules_generator.models import SourceryCustomRule
3+
4+
5+
def test_fully_qualified_function_name():
6+
result = expensive_loop.create_sourcery_custom_rules("custom_lib.api.create_item")
7+
8+
expected = (
9+
SourceryCustomRule(
10+
id="no-custom_lib-api-create_item-for",
11+
description="Don't call `custom_lib.api.create_item()` in loops.",
12+
tags=["performance", "no-custom_lib-api-create_item-in-loops"],
13+
pattern="""
14+
for ... in ... :
15+
...
16+
custom_lib.api.create_item(...)
17+
...
18+
""",
19+
),
20+
SourceryCustomRule(
21+
id="no-custom_lib-api-create_item-while",
22+
description="Don't call `custom_lib.api.create_item()` in loops.",
23+
tags=["performance", "no-custom_lib-api-create_item-in-loops"],
24+
pattern="""
25+
while ... :
26+
...
27+
custom_lib.api.create_item(...)
28+
...
29+
""",
30+
),
31+
)
32+
33+
assert result == expected

0 commit comments

Comments
 (0)