Skip to content

Commit 3c6f561

Browse files
authored
feat: dependencies: allow more importers closes #3 (#16)
"dependencies" template: Allow multiple importers separated by a "," Tests -------- * 2 importers * package name with _ version 0.5.0
1 parent 602a328 commit 3c6f561

File tree

11 files changed

+172
-15
lines changed

11 files changed

+172
-15
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ 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
11+
12+
* Dependencies rules: Allow multiple importer packages
13+
14+
### Added
15+
1016
## [0.4.0] - 2023-01-05
1117

1218
### Added

README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,15 @@ You'll be prompted to provide:
5959
* a package name
6060
* the packages that are allowed to import the package above
6161

62+
The 1st parameter is the fully qualified name of a package or module.
63+
It can be a package within your project or an external dependency.
64+
6265
The 2nd parameter is optional.
63-
E.g. it makes sense to say that no other package should import the `api` or `cli` package of your project.
66+
You have the following possibilities:
67+
68+
* 0 allowed importer (e.g. for packages like `api`, `cli`). Leave this parameter empty.
69+
* 1 allowed importer. Provide the importer package's fully qualified name.
70+
* Multiple allowed importers. Provide multiple fully qualified package names separated by a comma `,`
6471

6572
=>
6673

@@ -69,6 +76,11 @@ E.g. it makes sense to say that no other package should import the `api` or `cli
6976
* 1 for `import` statements
7077
* 1 for `from ... import` statements
7178

79+
Every generated rule allows imports:
80+
81+
* within the package itself
82+
* in tests
83+
7284
## Dependencies Use Cases
7385

7486
### Internal Dependencies Between the Packages of a Project

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.4.0"
3+
version = "0.5.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.4.0"
1+
__version__ = "0.5.0"

sourcery_rules_generator/cli/dependencies_cli.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
## Parameters for the "Dependencies" Template
2727
2828
1. The package that gets imported. Required.
29-
2. The package(s) that are allowed to import the package above. This parameter can be empty.
29+
2. The package(s) that are allowed to import the package above. This parameter can be empty. You can provide multiple fully qualified package names separated by comma `,`.
3030
"""
3131

3232

@@ -35,12 +35,12 @@ def create(
3535
package_option: str = typer.Option(
3636
None,
3737
"--package",
38-
help="The fully qualified name of the package",
38+
help="The fully qualified name of the package. Always exactly 1 package.",
3939
),
4040
caller_option: str = typer.Option(
4141
None,
4242
"--importer",
43-
help="The fully qualified name of the allowed importer",
43+
help="The fully qualified names of the allowed importers separated by comma. This can be 0, 1 or more importers.",
4444
),
4545
interactive_flag: bool = typer.Option(
4646
True,

sourcery_rules_generator/dependencies.py

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,30 @@ def create_yaml_rules(package: str, allowed_importer: Optional[str]) -> str:
1212
return yaml_converter.dumps(rules_dict)
1313

1414

15-
def create_sourcery_custom_rules(package: str, allowed_importer: Optional[str]) -> str:
15+
def create_sourcery_custom_rules(
16+
package: str, allowed_importer_text: Optional[str]
17+
) -> str:
1618
# Dots aren't allowed in the rule ID.
1719
package_slug = package.replace(".", "-")
1820

21+
# The dot in the package's fully qualified name
22+
# needs to be escaped in the regex used for the condition.
1923
package_in_regex = package.replace(".", "\.")
20-
package_path = package.replace(".", "/")
2124

22-
exclude_paths = [f"{package_path}/", "tests/"]
23-
description = f"Do not import `{package}` in other packages"
24-
if allowed_importer:
25-
description = f"Only `{allowed_importer}` should import `{package}`"
26-
allowed_importer_path = allowed_importer.replace(".", "/")
27-
exclude_paths.append(f"{allowed_importer_path}/")
25+
exclude_paths = [_path_for_package(package), "tests/"]
26+
27+
if allowed_importer_text:
28+
if "," in allowed_importer_text:
29+
allowed_importers = allowed_importer_text.split(",")
30+
description = _description_for_multiple_importers(
31+
package, allowed_importers
32+
)
33+
exclude_paths.extend(_path_for_package(impo) for impo in allowed_importers)
34+
else:
35+
description = _description_for_1_importer(package, allowed_importer_text)
36+
exclude_paths.append(_path_for_package(allowed_importer_text))
37+
else:
38+
description = _description_for_0_importer(package)
2839

2940
import_rule = SourceryCustomRule(
3041
id=f"dependency-rules-{package_slug}-import",
@@ -45,3 +56,20 @@ def create_sourcery_custom_rules(package: str, allowed_importer: Optional[str])
4556
)
4657

4758
return (import_rule, from_rule)
59+
60+
61+
def _description_for_0_importer(package: str):
62+
return f"Do not import `{package}` in other packages"
63+
64+
65+
def _description_for_1_importer(package: str, allowed_importer: str):
66+
return f"Only `{allowed_importer}` should import `{package}`"
67+
68+
69+
def _description_for_multiple_importers(package: str, allowed_importers: list[str]):
70+
quoted = [f"`{impo}`" for impo in allowed_importers]
71+
return f"Only {', '.join(quoted)} should import `{package}`"
72+
73+
74+
def _path_for_package(package: str) -> str:
75+
return package.replace(".", "/") + "/"

tests/conftest.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,11 @@ def yaml_rules_only_api_imports_core():
99
os.path.dirname(__file__),
1010
"fixtures/dependency-rules/core-imported-only-by-api.yaml",
1111
).read_text()
12+
13+
14+
@pytest.fixture
15+
def yaml_rules_only_db_and_db_util_import_sqlalchemy():
16+
return Path(
17+
os.path.dirname(__file__),
18+
"fixtures/dependency-rules/sqlalchemy-imported-only-by-2.yaml",
19+
).read_text()

tests/end-to-end/test_cli.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,24 @@ def test_create_dependency_rule_with_1_importer(yaml_rules_only_api_imports_core
1414
assert result.exit_code == 0
1515
assert "Generated YAML Rules" in result.stderr
1616
assert result.stdout == yaml_rules_only_api_imports_core
17+
18+
19+
def test_create_dependency_rule_with_2_importers(
20+
yaml_rules_only_db_and_db_util_import_sqlalchemy,
21+
):
22+
result = runner.invoke(
23+
app,
24+
[
25+
"dependencies",
26+
"create",
27+
"--package",
28+
"sqlalchemy",
29+
"--importer",
30+
"app.db,app.db_util",
31+
"--plain",
32+
],
33+
)
34+
35+
assert result.exit_code == 0
36+
assert "Generated YAML Rules" in result.stderr
37+
assert result.stdout == yaml_rules_only_db_and_db_util_import_sqlalchemy
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
rules:
2+
- id: dependency-rules-sqlalchemy-import
3+
description: Only `app.db`, `app.db_util` should import `sqlalchemy`
4+
pattern: import ..., ${module}, ...
5+
condition: module.matches_regex(r"^sqlalchemy\b")
6+
paths:
7+
exclude:
8+
- sqlalchemy/
9+
- tests/
10+
- app/db/
11+
- app/db_util/
12+
tags:
13+
- architecture
14+
- dependencies
15+
- id: dependency-rules-sqlalchemy-from
16+
description: Only `app.db`, `app.db_util` should import `sqlalchemy`
17+
pattern: from ${module} import ...
18+
condition: module.matches_regex(r"^sqlalchemy\b")
19+
paths:
20+
exclude:
21+
- sqlalchemy/
22+
- tests/
23+
- app/db/
24+
- app/db_util/
25+
tags:
26+
- architecture
27+
- dependencies
28+

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.4.0"
5+
assert __version__ == "0.5.0"

0 commit comments

Comments
 (0)