Skip to content

Commit

Permalink
feat: add exclude arg to file_patterns
Browse files Browse the repository at this point in the history
  • Loading branch information
jerivas committed Feb 28, 2022
1 parent ada18a9 commit 0d8f2fc
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 48 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,9 @@ myapp
├── <id>
│   ├── delete.py
│   ├── edit.py
│   └── index.py
│   └── __init__.py
├── add.py
└── index.py
└── __init__.py
3 directories, 5 files
```
Expand All @@ -105,7 +105,7 @@ This would generate the following URL patterns:

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

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:
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:

```python
"""
Expand Down
11 changes: 0 additions & 11 deletions demo/demo/urls_with_slash.py

This file was deleted.

20 changes: 12 additions & 8 deletions demo/demo/views_test.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
from pathlib import Path

import pytest
from django.urls import NoReverseMatch, reverse

from demo.models import Color


@pytest.fixture(autouse=True)
def change_test_dir(monkeypatch):
"""
For these tests change the CWD to the Django project root. This ensures the
view folder location works as expected in the call to `file_patterns` in
`urls.py`, even when pytest is called from the repo root.
"""
monkeypatch.chdir(str(Path(__file__).parent.parent))


@pytest.fixture
def color():
return Color.objects.create(name="Foo Color", slug="foo", code="00ff00")
Expand All @@ -17,14 +29,6 @@ def test_not_a_view(client):
assert response.status_code == 404


def test_append_slash(settings):
settings.ROOT_URLCONF = "demo.urls_with_slash"
assert reverse("home") == "/"
assert reverse("colors") == "/colors/"
assert reverse("colors_add") == "/colors/add/"
assert reverse("colors_slug", args=["abc"]) == "/colors/abc/"


def test_home(client):
url = reverse("home")
assert (
Expand Down
52 changes: 26 additions & 26 deletions file_router/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import os
import pathlib
import re
from importlib import import_module

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


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

module_path = f"{root}/{file}".replace(".py", "").replace("/", ".")
module = import_module(module_path)
view_fn = getattr(module, "view", None)
if not callable(view_fn):
continue
module_path = str(file).replace(".py", "").replace("/", ".")
module = import_module(module_path)
view_fn = getattr(module, "view", None)
if not callable(view_fn):
continue

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

try:
urlname = view_fn.urlname
except AttributeError:
urlname = DISALLOWED_CHARS.sub("", TO_UNDERSCORES.sub("_", url))
try:
urlname = view_fn.urlname
except AttributeError:
urlname = DISALLOWED_CHARS.sub("", TO_UNDERSCORES.sub("_", url))

patterns.append(path(url, view_fn, name=urlname))
patterns.append(path(url, view_fn, name=urlname))
return patterns


Expand Down
36 changes: 36 additions & 0 deletions file_router/file_router_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import shutil

import pytest

from . import file_patterns


@pytest.fixture(scope="session", autouse=True)
def copy_views():
"""Copy the views folder of the demo project to this folder"""
shutil.copytree("demo/demo/views", "views")
yield
shutil.rmtree("views")


def test_append_slash():
patterns = file_patterns("views", append_slash=True, exclude="")
output = [(str(p.pattern), p.name) for p in patterns]
assert output == [
("current-time/", "current_time"),
("colors/add/", "colors_add"),
("colors/", "colors"),
("colors/<slug:slug>/", "colors_slug"),
("", "home"),
]


def test_exclude():
patterns = file_patterns("views", append_slash=False, exclude="*-time.py")
output = [(str(p.pattern), p.name) for p in patterns]
assert output == [
("colors/add", "colors_add"),
("colors", "colors"),
("colors/<slug:slug>", "colors_slug"),
("", "home"),
]

0 comments on commit 0d8f2fc

Please sign in to comment.