Skip to content

Commit 0d8f2fc

Browse files
committed
feat: add exclude arg to file_patterns
1 parent ada18a9 commit 0d8f2fc

File tree

5 files changed

+77
-48
lines changed

5 files changed

+77
-48
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,9 @@ myapp
8888
├── <id>
8989
│   ├── delete.py
9090
│   ├── edit.py
91-
│   └── index.py
91+
│   └── __init__.py
9292
├── add.py
93-
└── index.py
93+
└── __init__.py
9494
9595
3 directories, 5 files
9696
```
@@ -105,7 +105,7 @@ This would generate the following URL patterns:
105105

106106
Each file now holds all the pieces required to perform a given action and requires much less context switching.
107107

108-
Notice that special placeholders like `<id>` are parsed as expected by Django's [`path`](https://docs.djangoproject.com/en/4.0/topics/http/urls/#how-django-processes-a-request) function, which means you can use path converters by including them in file and folder names such as `<int:id>`. For example, to get a single instance enforcing an integer `id` create a file `myapp/views/mymodel/<int:id>/index.py` with the code:
108+
Notice that special placeholders like `<id>` are parsed as expected by Django's [`path`](https://docs.djangoproject.com/en/4.0/topics/http/urls/#how-django-processes-a-request) function, which means you can use path converters by including them in file and folder names such as `<int:id>`. For example, to get a single instance enforcing an integer `id` create a file `myapp/views/mymodel/<int:id>/__init__.py` with the code:
109109

110110
```python
111111
"""

demo/demo/urls_with_slash.py

Lines changed: 0 additions & 11 deletions
This file was deleted.

demo/demo/views_test.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
1+
from pathlib import Path
2+
13
import pytest
24
from django.urls import NoReverseMatch, reverse
35

46
from demo.models import Color
57

68

9+
@pytest.fixture(autouse=True)
10+
def change_test_dir(monkeypatch):
11+
"""
12+
For these tests change the CWD to the Django project root. This ensures the
13+
view folder location works as expected in the call to `file_patterns` in
14+
`urls.py`, even when pytest is called from the repo root.
15+
"""
16+
monkeypatch.chdir(str(Path(__file__).parent.parent))
17+
18+
719
@pytest.fixture
820
def color():
921
return Color.objects.create(name="Foo Color", slug="foo", code="00ff00")
@@ -17,14 +29,6 @@ def test_not_a_view(client):
1729
assert response.status_code == 404
1830

1931

20-
def test_append_slash(settings):
21-
settings.ROOT_URLCONF = "demo.urls_with_slash"
22-
assert reverse("home") == "/"
23-
assert reverse("colors") == "/colors/"
24-
assert reverse("colors_add") == "/colors/add/"
25-
assert reverse("colors_slug", args=["abc"]) == "/colors/abc/"
26-
27-
2832
def test_home(client):
2933
url = reverse("home")
3034
assert (

file_router/__init__.py

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import os
1+
import pathlib
22
import re
33
from importlib import import_module
44

@@ -21,40 +21,40 @@
2121
TO_UNDERSCORES = re.compile("[/-]") # Slash and dash
2222

2323

24-
def file_patterns(start_dir, append_slash=False):
24+
def file_patterns(start_dir: str, append_slash: bool = False, exclude: str = ""):
2525
"""
2626
Create urlpatterns from a directory structure
2727
"""
2828
patterns = []
2929
start_dir_re = re.compile(f"^{start_dir}")
30-
for root, dirs, files in os.walk(start_dir):
31-
# Reverse-sort the list so files that start with "<" go to the bottom
32-
# and regular files come to the top. This ensures hard-coded url params
33-
# always match before variable ones like <pk> and <slug>
34-
files = sorted(files, reverse=True)
35-
for file in files:
36-
if not file.endswith(".py"):
37-
continue
30+
files = pathlib.Path(start_dir).glob("**/*.py")
31+
# Reverse-sort the list so files that start with "<" go to the bottom
32+
# and regular files come to the top. This ensures hard-coded url params
33+
# always match before variable ones like <pk> and <slug>
34+
files = sorted(files, reverse=True, key=str)
35+
for file in files:
36+
if exclude and pathlib.Path.match(file, exclude):
37+
continue
3838

39-
module_path = f"{root}/{file}".replace(".py", "").replace("/", ".")
40-
module = import_module(module_path)
41-
view_fn = getattr(module, "view", None)
42-
if not callable(view_fn):
43-
continue
39+
module_path = str(file).replace(".py", "").replace("/", ".")
40+
module = import_module(module_path)
41+
view_fn = getattr(module, "view", None)
42+
if not callable(view_fn):
43+
continue
4444

45-
try:
46-
url = view_fn.url
47-
except AttributeError:
48-
url = "" if file == "__init__.py" else file.replace(".py", "")
49-
url = start_dir_re.sub("", f"{root}/{url}").strip("/")
50-
url = (url + "/") if append_slash and url != "" else url
45+
try:
46+
url = view_fn.url
47+
except AttributeError:
48+
url = "" if file.name == "__init__.py" else file.name.replace(".py", "")
49+
url = start_dir_re.sub("", f"{file.parent}/{url}").strip("/")
50+
url = (url + "/") if append_slash and url != "" else url
5151

52-
try:
53-
urlname = view_fn.urlname
54-
except AttributeError:
55-
urlname = DISALLOWED_CHARS.sub("", TO_UNDERSCORES.sub("_", url))
52+
try:
53+
urlname = view_fn.urlname
54+
except AttributeError:
55+
urlname = DISALLOWED_CHARS.sub("", TO_UNDERSCORES.sub("_", url))
5656

57-
patterns.append(path(url, view_fn, name=urlname))
57+
patterns.append(path(url, view_fn, name=urlname))
5858
return patterns
5959

6060

file_router/file_router_test.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import shutil
2+
3+
import pytest
4+
5+
from . import file_patterns
6+
7+
8+
@pytest.fixture(scope="session", autouse=True)
9+
def copy_views():
10+
"""Copy the views folder of the demo project to this folder"""
11+
shutil.copytree("demo/demo/views", "views")
12+
yield
13+
shutil.rmtree("views")
14+
15+
16+
def test_append_slash():
17+
patterns = file_patterns("views", append_slash=True, exclude="")
18+
output = [(str(p.pattern), p.name) for p in patterns]
19+
assert output == [
20+
("current-time/", "current_time"),
21+
("colors/add/", "colors_add"),
22+
("colors/", "colors"),
23+
("colors/<slug:slug>/", "colors_slug"),
24+
("", "home"),
25+
]
26+
27+
28+
def test_exclude():
29+
patterns = file_patterns("views", append_slash=False, exclude="*-time.py")
30+
output = [(str(p.pattern), p.name) for p in patterns]
31+
assert output == [
32+
("colors/add", "colors_add"),
33+
("colors", "colors"),
34+
("colors/<slug:slug>", "colors_slug"),
35+
("", "home"),
36+
]

0 commit comments

Comments
 (0)