diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..027aac6 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,65 @@ +name: lint + +on: + push: + pull_request: + workflow_dispatch: + inputs: + debug: + description: 'Set to on, to open ssh debug session.' + required: true + default: 'off' + +jobs: + + lint: + runs-on: ubuntu-latest + strategy: + matrix: + # run static analysis on bleeding and trailing edges + python-version: [ '3.9', '3.12' ] + django-version: + - 'Django~=3.2.0' # LTS April 2024 + - 'Django~=4.2.0' # LTS April 2026 + - 'Django~=5.0.0' + exclude: + - python-version: '3.9' + django-version: 'Django~=4.2.0' + - python-version: '3.9' + django-version: 'Django~=5.0.0' + - python-version: '3.12' + django-version: 'Django~=3.2.0' + + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + version: 1.8.3 + virtualenvs-create: true + virtualenvs-in-project: true + - name: Install Dependencies + run: | + poetry config virtualenvs.in-project true + poetry run pip install --upgrade pip + poetry install -E all + poetry run pip install -U "${{ matrix.django-version }}" + - name: Install Emacs + if: ${{ github.event.inputs.debug == 'on' }} + run: | + sudo apt install emacs + - name: Setup tmate session + if: ${{ github.event.inputs.debug == 'on' }} + uses: mxschmitt/action-tmate@v3 + with: + detached: true + timeout-minutes: 60 + - name: Run Static Analysis + run: | + ./check.sh --no-fix + poetry run python -m readme_renderer ./README.md -o /tmp/README.html diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 48d9524..dbd0335 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,57 +1,19 @@ name: test -on: [push, pull_request, workflow_dispatch] +on: + push: + pull_request: + workflow_dispatch: + inputs: + debug: + description: 'Set to on, to open ssh debug session.' + required: true + default: 'off' -jobs: - - static-analysis: - runs-on: ubuntu-latest - strategy: - matrix: - # run static analysis on bleeding and trailing edges - python-version: [ '3.9', '3.12' ] - django-version: - - 'Django~=3.2.0' # LTS April 2024 - - 'Django~=4.2.0' # LTS April 2026 - - 'Django~=5.0.0' - exclude: - - python-version: '3.9' - django-version: 'Django~=4.2.0' - - python-version: '3.9' - django-version: 'Django~=5.0.0' - - python-version: '3.12' - django-version: 'Django~=3.2.0' - - steps: - - uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} + schedule: + - cron: '0 13 * * *' # Runs at 6 am pacific every day - - name: Install Poetry - uses: snok/install-poetry@v1 - with: - version: 1.5.1 - virtualenvs-create: true - virtualenvs-in-project: true - - name: Install Dependencies - run: | - poetry config virtualenvs.in-project true - poetry run pip install --upgrade pip - poetry install -E all - poetry run pip install -U "${{ matrix.django-version }}" - - name: Run Static Analysis - run: | - poetry run black render_static --check - poetry run pylint render_static - poetry run mypy render_static - poetry check - poetry run pip check - poetry export --without-hashes --format=requirements.txt | poetry run safety check --stdin - poetry run python -m readme_renderer ./README.md -o /tmp/README.html - cd ./doc - poetry run doc8 --ignore-path build --max-line-length 100 +jobs: test: runs-on: ubuntu-latest @@ -93,17 +55,22 @@ jobs: poetry run pip install --upgrade pip poetry install -E all poetry run pip install -U "Django~=${{ matrix.django-version }}" + - name: Install Emacs + if: ${{ github.event.inputs.debug == 'on' }} + run: | + sudo apt install emacs + - name: Setup tmate session + if: ${{ github.event.inputs.debug == 'on' }} + uses: mxschmitt/action-tmate@v3 + with: + detached: true + timeout-minutes: 60 - name: Run Unit Tests run: | poetry run pytest poetry run pip uninstall -y jinja2 pyyaml importlib-resources poetry run pytest --cov-append mv .coverage py${{ matrix.python-version }}-dj${{ matrix.django-version }}.coverage - # - name: Setup tmate session - # uses: mxschmitt/action-tmate@v3 - # with: - # detached: true - # timeout-minutes: 60 - name: Store coverage files uses: actions/upload-artifact@v4 with: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8c1b64a..f91a555 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,12 +45,7 @@ static analysis tools should not produce any errors or warnings. Disabling certa warnings where justified is acceptable: ```shell -poetry run isort render_static -poetry run black render_static -poetry run mypy render_static -poetry run pylint render_static -poetry check -poetry run pip check +./check.sh poetry run python -m readme_renderer ./README.md ``` @@ -59,7 +54,7 @@ poetry run python -m readme_renderer ./README.md `django-render-static` is setup to use [django-pytest](https://pytest-django.readthedocs.io/en/latest/) to allow [pytest](https://docs.pytest.org/en/stable/) to run Django unit tests. All the tests are housed in -render_static/tests/tests.py. Before a PR is accepted, all tests must be passing and the code +tests/tests.py. Before a PR is accepted, all tests must be passing and the code coverage must be at 100%. To run the full suite: @@ -78,6 +73,6 @@ For instance to run all tests in DefinesToJavascriptTest, and then just the test you would do: ```shell -poetry run pytest render_static/tests/tests.py::DefinesToJavascriptTest -poetry run pytest render_static/tests/tests.py::DefinesToJavascriptTest::test_classes_to_js +poetry run pytest tests/tests.py::DefinesToJavascriptTest +poetry run pytest tests/tests.py::DefinesToJavascriptTest::test_classes_to_js ``` diff --git a/README.md b/README.md index 6720bee..5925833 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ [![Documentation Status](https://readthedocs.org/projects/django-render-static/badge/?version=latest)](http://django-render-static.readthedocs.io/?badge=latest/) [![Code Cov](https://codecov.io/gh/bckohan/django-render-static/branch/main/graph/badge.svg?token=0IZOKN2DYL)](https://codecov.io/gh/bckohan/django-render-static) [![Test Status](https://github.com/bckohan/django-render-static/workflows/test/badge.svg)](https://github.com/bckohan/django-render-static/actions/workflows/test.yml) +[![Lint Status](https://github.com/bckohan/django-render-static/workflows/lint/badge.svg)](https://github.com/bckohan/django-render-static/actions/workflows/lint.yml) [![Code Style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) # django-render-static diff --git a/check.sh b/check.sh new file mode 100755 index 0000000..af90891 --- /dev/null +++ b/check.sh @@ -0,0 +1,31 @@ +set -e # Exit immediately if a command exits with a non-zero status. + +if [ "$1" == "--no-fix" ]; then + poetry run ruff format --check + poetry run ruff check --select I + poetry run ruff check +else + poetry run ruff format + poetry run ruff check --fix --select I + poetry run ruff check --fix +fi + +poetry run mypy render_static +poetry run pyright +poetry check +poetry run pip check +cd ./doc +poetry run doc8 --ignore-path build --max-line-length 100 -q +# check for broken links in the docs ############ +set +e + +# do not run this in CI - too spurious +if [ "$1" != "--no-fix" ]; then + poetry run sphinx-build -b linkcheck -q ./source ./build > /dev/null 2>&1 + if [ $? -ne 0 ]; then + cat ./build/output.txt | grep broken + exit 1 + fi +fi +################################################# +cd .. diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index 829dbb4..7d63a87 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -2,6 +2,20 @@ Change Log ========== +v3.0.0 +====== + +**This is a major version upgrade - please see migration guide for instructions +on how to** :ref:`migrate from version 2.x to 3.x. ` + +* Implemented `Move tests into top level directory. `_ +* Implemented `Remove wrapped dependency required mishegoss and replace with jinja2 module level imports `_ +* Implemented `Allow context import path to also point to a module. `_ +* Implemented `Remove imports in __init__.py `_ +* Implemented `Switch to ruff for formatting and linting `_ +* Fixed `Support django-typer version 2.1 `_ + + v2.2.1 ====== @@ -61,7 +75,7 @@ v2.0.0 ====== **This is a major version upgrade - please see migration guide for instructions -on how to** :doc:`migration` **from version 1.x to 2.x.** +on how to** :ref:`migrate from version 1.x to 2.x. ` * Implemented `Add some default templates to ship for defines, urls and enums. `_ * Implemented `Generate JDoc comments in the generated URLResolver class. `_ diff --git a/doc/source/commands.rst b/doc/source/commands.rst index efdf64a..746d51c 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -17,6 +17,7 @@ Usage .. typer:: render_static.management.commands.renderstatic.Command:typer_app :prog: manage.py renderstatic :width: 90 + :theme: dark Example ~~~~~~~ diff --git a/doc/source/conf.py b/doc/source/conf.py index 3bbe6ce..2fd4464 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -3,10 +3,11 @@ import os from pathlib import Path sys.path.append(str(Path(__file__).parent.parent.parent)) +sys.path.append(str(Path(__file__).parent.parent.parent / 'tests')) import render_static import django -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'render_static.tests.settings') +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tests.settings') django.setup() # Configuration file for the Sphinx documentation builder. diff --git a/doc/source/configuration.rst b/doc/source/configuration.rst index 19d3ac0..811dca1 100644 --- a/doc/source/configuration.rst +++ b/doc/source/configuration.rst @@ -79,7 +79,7 @@ here, instead use the two engines provided by `django-render-static`: - ``render_static.backends.StaticDjangoTemplates`` - default app directory: ``static_templates`` -- ``render_static.backends.StaticJinja2Templates`` +- ``render_static.backends.jinja2.StaticJinja2Templates`` - default app directory: ``static_jinja2`` If ``APP_DIRS`` is true, or if an app directories loader is used such that templates are searched @@ -111,7 +111,7 @@ template backend the loaders have been extended and static specific loaders shou - ``render_static.loaders.django.StaticFilesystemLoader`` - ``render_static.loaders.django.StaticLocMemLoader`` -- ``render_static.backends.StaticJinja2Templates`` +- ``render_static.backends.jinja2.StaticJinja2Templates`` - ``render_static.loaders.jinja2.StaticFileSystemBatchLoader`` **default** - ``render_static.loaders.jinja2.StaticFileSystemLoader`` - ``render_static.loaders.jinja2.StaticPackageLoader`` @@ -156,6 +156,8 @@ Context configuration parameters may be any of the following: - **dictionary**: Simply specify context dictionary inline - **callable**: That returns a dictionary. This allows lazy context initialization to take place after Django bootstrapping + - **module**: When a module is used as a context, the module's locals will be used as the + context. - **json**: A path to a JSON file - **yaml**: A path to a YAML file (yaml supports comments!) - **pickle**: A path to a python pickled dictionary @@ -278,7 +280,7 @@ And our settings file might look like: STATIC_TEMPLATES = { 'ENGINES': [{ - 'BACKEND': 'render_static.backends.StaticJinja2Templates', + 'BACKEND': 'render_static.backends.jinja2.StaticJinja2Templates', 'OPTIONS': { 'loader': StaticFileSystemBatchLoader() }, diff --git a/doc/source/index.rst b/doc/source/index.rst index 84cd779..0c3a826 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -30,7 +30,7 @@ and Jinja templates and allows contexts to be specified in python, json or YAML. You can report bugs and discuss features on the `issues page `_. -`Contributions `_ are +`Contributions `_ are encouraged! Especially additional template tags and filters! .. toctree:: diff --git a/doc/source/migration.rst b/doc/source/migration.rst index 4935a32..37f01fc 100644 --- a/doc/source/migration.rst +++ b/doc/source/migration.rst @@ -8,6 +8,22 @@ Migration page documents how to migrate past the breaking changes introduced by major version updates. +.. _migration_2_3: + +2.x -> 3.x +---------- + +There are some import path changes in 3.0: + +* ``render_static.ClassURLWriter`` -> ``render_static.transpilers.ClassURLWriter`` +* ``render_static.SimpleURLWriter`` -> ``render_static.transpilers.SimpleURLWriter`` +* ``render_static.EnumClassWriter`` -> ``render_static.transpilers.EnumClassWriter`` +* ``render_static.DefaultDefineTranspiler`` -> ``render_static.transpilers.DefaultDefineTranspiler`` +* ``render_static.backends.StaticJinja2Templates`` -> + ``render_static.backends.jinja2.StaticJinja2Templates`` + +.. _migration_1_2: + 1.x -> 2.x ---------- diff --git a/doc/source/reference.rst b/doc/source/reference.rst index 2357f68..fcb56ce 100644 --- a/doc/source/reference.rst +++ b/doc/source/reference.rst @@ -18,9 +18,12 @@ engine backends ---------------------------------- -.. automodule:: render_static.backends +.. automodule:: render_static.backends.django .. autoclass:: StaticDjangoTemplates + +.. automodule:: render_static.backends.jinja2 + .. autoclass:: StaticJinja2Templates @@ -114,7 +117,7 @@ placeholders transpilers ---------------------------------------------------------------- -.. automodule:: render_static.transpilers +.. automodule:: render_static.transpilers.base .. autofunction:: to_js .. autofunction:: to_js_datetime diff --git a/doc/source/templatetags.rst b/doc/source/templatetags.rst index 69d8ac8..97eac93 100644 --- a/doc/source/templatetags.rst +++ b/doc/source/templatetags.rst @@ -299,7 +299,7 @@ specific url names. For instance: .. code-block:: js+django - {% urls_to_js transpiler='render_static.SimpleURLWriter' %} + {% urls_to_js transpiler='render_static.transpilers.SimpleURLWriter' %} {% override 'namespace:path_name' %} return "/an/overridden/path"; @@ -317,7 +317,7 @@ class writer: .. code-block:: htmldjango - {% urls_to_js transpiler='render_static.ClassURLWriter' class_name='URLResolver' %} + {% urls_to_js transpiler='render_static.transpilers.ClassURLWriter' class_name='URLResolver' %} {% urls_to_js %} diff --git a/manage.py b/manage.py index 1701f21..8d8beb8 100755 --- a/manage.py +++ b/manage.py @@ -5,7 +5,7 @@ def main(): - os.environ['DJANGO_SETTINGS_MODULE'] = 'render_static.tests.settings' + os.environ["DJANGO_SETTINGS_MODULE"] = "tests.settings" management.execute_from_command_line() diff --git a/pyproject.toml b/pyproject.toml index f39cbe9..2af0eef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "django-render-static" -version = "2.2.1" +version = "3.0.0" description = "Use Django's template engine to render static files at deployment or package time. Includes transpilers for extending Django's url reversal and enums to JavaScript." authors = ["Brian Kohan "] license = "MIT" @@ -37,7 +37,6 @@ classifiers = [ packages = [ { include = "render_static" } ] -exclude = ["render_static/tests"] # [tool.poetry.scripts] # django-renderstatic = 'render_static.console:main' @@ -48,36 +47,31 @@ Django = ">=3.2,<6.0" importlib-resources = { version = ">=1.3.0", python = "<3.9" } Jinja2 = { version = ">=2.9,<4.0", optional = true } PyYAML = { version = ">=5.1,<7.0", optional = true } -django-typer = "^1.0.0" +django-typer = "^2.1.1" [tool.poetry.group.dev.dependencies] django-enum = "^1.1.0" enum-properties = "^1.1.1" -pytest = "^7.0" -pytest-django = "^4.7.0" +pytest = "^8.0" +pytest-django = "^4.8.0" Sphinx = "^7.0.0" -sphinx-js = [ - { version = "<3.0", markers = "python_version < '3.8'" }, - { version = "^3.0", markers = "python_version >= '3.8'" }, -] sphinx-rtd-theme = "^2.0.0" mypy = "^1.8" -isort = "^5.13.0" doc8 = "^1.1.0" pytest-cov = "^4.1.0" -pylint = "^3.0.0" deepdiff = "^6.7.0" -safety = "^2.3.0" readme-renderer = {extras = ["md"], version = "^43.0"} types-PyYAML = "^6.0" -coverage = "^7.3.0" +coverage = "^7.5.0" importlib-metadata = "^7.0.0" selenium = "^4.16.0" python-dateutil = "^2.8.2" ipdb = "^0.13.13" -black = "^23.12.0" aiohttp = "^3.9.1" -sphinxcontrib-typer = "^0.1.11" +sphinxcontrib-typer = {extras = ["html", "pdf", "png"], version = "^0.3.0", markers="python_version >= '3.9'"} +pyright = "^1.1.366" +ruff = "^0.4.8" +django-stubs = {extras = ["compatible-mypy"], version = "^5.0.2"} [build-system] requires = ["poetry-core>=1.0.0"] @@ -108,36 +102,17 @@ warn_redundant_casts = true warn_unused_configs = true warn_unreachable = true warn_no_return = true -exclude = "tests" + [tool.doc8] ignore-path = "doc/build" max-line-length = 100 sphinx = true -[tool.isort] -profile = "black" - -[tool.pylint] -output-format = "colorized" -max-line-length = 88 # PEP 8 -max-args = 7 - -[tool.pylint.'MESSAGES CONTROL'] -disable = "R0903, R0801" [tool.pytest.ini_options] -DJANGO_SETTINGS_MODULE = "render_static.tests.settings" -python_files = [ - "tests.py", - "yaml_tests.py", - "jinja2_tests.py", - "js_tests.py", - "examples_tests.py", - "traverse_tests.py" - # "web_tests.py" -] +DJANGO_SETTINGS_MODULE = "tests.settings" norecursedirs = "*.egg .eggs dist build docs .tox .git __pycache__" addopts = [ "--strict-markers", @@ -148,15 +123,27 @@ addopts = [ [tool.coverage.run] omit = [ - "render_static/tests/app1/static_jinja2/batch_test/**/*", - "render_static/tests/app1/static_jinja2/**/*", - "render_static/tests/app2/static_jinja2/**/*" + "tests/app1/static_jinja2/batch_test/**/*", + "tests/app1/static_jinja2/**/*", + "tests/app2/static_jinja2/**/*" ] -[tool.black] +[tool.ruff] line-length = 88 -target-version = ["py38", "py39", "py310", "py311", "py312"] -include = '\.pyi?$' -extend-exclude = ''' - render_static/tests/resources/bad_code\.py -''' +exclude = [ + "doc", + "dist", + "examples", + "tests/resources/bad_code.py" +] + +[tool.ruff.lint] +exclude = [ + "tests/**/*", +] + +[tool.pyright] +exclude = ["tests/**/*"] +include = [ + "render_static" +] diff --git a/render_static/__init__.py b/render_static/__init__.py index e980a9b..366fd16 100755 --- a/render_static/__init__.py +++ b/render_static/__init__.py @@ -6,38 +6,11 @@ /_/ |_|\___/_/ /_/\__,_/\___/_/ /____/\__/\__,_/\__/_/\___/ """ -from .context import resolve_context -from .resource import resource -from .transpilers.defines_to_js import DefaultDefineTranspiler -from .transpilers.enums_to_js import EnumClassWriter -from .transpilers.urls_to_js import ClassURLWriter, SimpleURLWriter -VERSION = (2, 2, 1) +VERSION = (3, 0, 0) __title__ = "Django Render Static" __version__ = ".".join(str(i) for i in VERSION) __author__ = "Brian Kohan" __license__ = "MIT" __copyright__ = "Copyright 2020-2024 Brian Kohan" - - -__all__ = [ - "resource", - "resolve_context", - "DefaultDefineTranspiler", - "EnumClassWriter", - "ClassURLWriter", - "SimpleURLWriter", -] - - -class Jinja2DependencyNeeded: # pylint: disable=R0903 - """ - Jinja2 is an optional dependency - lazy fail if its use is attempted - without it being present on the python path. - """ - - def __init__(self, *args, **kwargs): - raise ImportError( - "To use the Jinja2 backend you must install the Jinja2 python package." - ) diff --git a/render_static/apps.py b/render_static/apps.py index d30bad9..3046ba0 100755 --- a/render_static/apps.py +++ b/render_static/apps.py @@ -1,7 +1,5 @@ -# pylint: disable=C0114 from django.apps import AppConfig class RenderStaticConfig(AppConfig): - # pylint: disable=C0115 name = "render_static" diff --git a/render_static/backends.py b/render_static/backends.py deleted file mode 100755 index 88544f0..0000000 --- a/render_static/backends.py +++ /dev/null @@ -1,253 +0,0 @@ -""" -Extensions of the standard Django template backends that add a few more -configuration parameters and functionality necessary for the static engine. -These backends should be used instead of the standard backends! -""" -from os.path import normpath -from pathlib import Path -from typing import Dict, Generator, List, Tuple - -from django.apps import apps -from django.apps.config import AppConfig -from django.template import Template -from django.template.backends.django import DjangoTemplates, TemplateDoesNotExist - -from render_static import Jinja2DependencyNeeded -from render_static.loaders.django import SearchableLoader -from render_static.loaders.jinja2 import SearchableLoader as SearchableJinja2Loader -from render_static.loaders.jinja2 import StaticFileSystemBatchLoader -from render_static.origin import AppOrigin -from render_static.templatetags import render_static - -__all__ = ["StaticDjangoTemplates", "StaticJinja2Templates"] - - -class StaticDjangoTemplates(DjangoTemplates): - """ - Extend the standard ``django.template.backends.django.DjangoTemplates`` - backend to add options and change the default loaders. - - By default this backend will search for templates in application - directories named ``static_templates``. The ``app_dir`` option is added to - the standard options to allow users to override this location. - - :param params: The parameters as passed into the ``STATIC_TEMPLATES`` - configuration for this backend. - """ - - app_dirname = "static_templates" - - def __init__(self, params: Dict) -> None: - params = params.copy() - options = params.pop("OPTIONS").copy() - loaders = options.get("loaders", None) - options.setdefault("builtins", ["render_static.templatetags.render_static"]) - self.app_dirname = options.pop("app_dir", self.app_dirname) - if loaders is None: - loaders = ["render_static.loaders.StaticFilesystemLoader"] - if params.get("APP_DIRS", False): - loaders += ["render_static.loaders.StaticAppDirectoriesLoader"] - # base class with throw if this isn't present, it must be false - params["APP_DIRS"] = False - options["loaders"] = loaders - params["OPTIONS"] = options - super().__init__(params) - self.engine.app_dirname = self.app_dirname - - def select_templates( - self, selector: str, first_loader: bool = False, first_preference: bool = False - ) -> List[str]: - """ - Resolves a template selector into a list of template names from the - loaders configured on this backend engine. - - :param selector: The template selector - :param first_loader: If True, return only the set of template names - from the first loader that matches any part of the selector. By - default (False) any template name that matches the selector from - any loader will be returned. - :param first_preference: If true, return only the templates that match - the first preference for each loader. When combined with - first_loader will return only the first preference(s) of the first - loader. Preferences are loader specific and documented on the - loader. - :return: The list of resolved template names - """ - template_names = set() - for loader in self.engine.template_loaders: - try: - if callable(getattr(loader, "select_templates", None)): - for templates in loader.select_templates(selector): - for tmpl in templates: - template_names.add(tmpl) - if templates and first_preference: - break - else: - loader.get_template(selector) - template_names.add(selector) - if first_loader and template_names: - return list(template_names) - except TemplateDoesNotExist: - continue - if template_names: - return list(template_names) - raise TemplateDoesNotExist( - f"Template selector {selector} did not resolve to any template " f"names." - ) - - def search_templates( - self, prefix: str, first_loader: bool = False - ) -> Generator[Template, None, None]: - """ - Resolves a partial template selector into a list of template names from the - loaders configured on this backend engine. - - :param prefix: The template prefix to search for - :param first_loader: Search only the first loader - :return: The list of resolved template names - """ - for loader in self.engine.template_loaders[: 1 if first_loader else None]: - if isinstance(loader, SearchableLoader): - yield from loader.search(prefix) - - -try: - from django.template.backends.jinja2 import Jinja2 # pylint: disable=C0412 - from django.template.backends.jinja2 import Template as Jinja2Template - from jinja2 import Environment - - def default_env(**options): - """ - The default Jinja2 backend environment. This environment adds the tags - and filters from render_static. - - :param options: - :return: - """ - env = Environment(**options) - env.globals.update(render_static.register.filters) - env.globals.update( - { - name: getattr(tag, "__wrapped__", tag) - for name, tag in render_static.register.tags.items() - } - ) - return env - - class StaticJinja2Templates(Jinja2): - """ - Extend the standard ``django.template.backends.jinja2.Jinja2`` backend - to add options. Unlike with the standard backend, the loaders used for - this backend remain unchanged. - - By default this backend will search for templates in application - directories named ``static_jinja2``. The ``app_dir`` option is added to - the standard option to allow users to override this location. - - :param params: The parameters as passed into the ``STATIC_TEMPLATES`` - configuration for this backend. - """ - - app_dirname = "static_jinja2" - app_directories: List[Tuple[Path, AppConfig]] = [] - - def __init__(self, params: Dict) -> None: - params = params.copy() - self.dirs = list(params.get("DIRS", [])) - self.app_dirs = params.get("APP_DIRS", False) - options = params.pop("OPTIONS").copy() - options.setdefault("environment", "render_static.backends.default_env") - self.app_dirname = options.pop("app_dir", self.app_dirname) - - if "loader" not in options: - options["loader"] = StaticFileSystemBatchLoader(self.template_dirs) - - params["OPTIONS"] = options - - self.app_directories = [ - (Path(app_config.path) / self.app_dirname, app_config) - for app_config in apps.get_app_configs() - if app_config.path - and (Path(app_config.path) / self.app_dirname).is_dir() - ] - - super().__init__(params) - - def get_template(self, template_name: str) -> Jinja2Template: - """ - We override the Jinja2 get_template method so we can monkey patch - in the AppConfig of the origin if this template was from an app - directory. This information is used later down the line when - deciding where to write rendered templates. For the django template - backend we modified the loaders but modifying the Jinja2 loaders - would be much more invasive. - """ - template = super().get_template(template_name) - - for app_dir, app in self.app_directories: - if normpath(template.origin.name).startswith(normpath(app_dir)): - template.origin = AppOrigin( - name=template.origin.name, - template_name=template.origin.template_name, - app=app, - ) - break - return template - - def select_templates( - self, - selector: str, - first_loader: bool = False, # pylint: disable=unused-argument - first_preference: bool = False, - ) -> List[str]: - """ - Resolves a template selector into a list of template names from - the loader configured on this backend engine. - - :param selector: The template selector - :param first_loader: This is ignored for the Jinja2 engine. The - Jinja2 engine only has one loader. - :param first_preference: If true, return only the templates that - match the first preference for the loader. Preferences are - loader specific and documented on the loader. - :return: The list of resolved template names - """ - template_names = set() - if callable(getattr(self.env.loader, "select_templates", None)): - for templates in self.env.loader.select_templates(selector): - if templates: - for tmpl in templates: - template_names.add(tmpl) - if first_preference: - break - else: - self.get_template(selector) - template_names.add(selector) - - if template_names: - return list(template_names) - - raise TemplateDoesNotExist( - f"Template selector {selector} did not resolve to any " - f"template names." - ) - - def search_templates( - self, - prefix: str, - first_loader: bool = False, # pylint: disable=unused-argument - ) -> Generator[Jinja2Template, None, None]: - """ - Resolves a partial template selector into a list of template names from the - loaders configured on this backend engine. - - :param prefix: The template prefix to search for - :param first_loader: This is ignored for the Jinja2 engine because there is - only one loader - :return: The list of resolved template names - """ - if isinstance(self.env.loader, SearchableJinja2Loader): - yield from self.env.loader.search(self.env, prefix) - -except ImportError: - StaticJinja2Templates = Jinja2DependencyNeeded # type: ignore diff --git a/render_static/backends/__init__.py b/render_static/backends/__init__.py new file mode 100644 index 0000000..f1995ab --- /dev/null +++ b/render_static/backends/__init__.py @@ -0,0 +1,3 @@ +from .django import StaticDjangoTemplates + +__all__ = ["StaticDjangoTemplates"] diff --git a/render_static/backends/base.py b/render_static/backends/base.py new file mode 100644 index 0000000..ba083bb --- /dev/null +++ b/render_static/backends/base.py @@ -0,0 +1,57 @@ +import typing as t +from abc import abstractmethod + +from django.template import Template +from django.template.backends.base import BaseEngine + +T = t.TypeVar("T") + + +def with_typehint(baseclass: t.Type[T]) -> t.Type[T]: + """ + Change inheritance to add Field type hints when type checking is running. + This is just more simple than defining a Protocol - revisit if Django + provides Field protocol - should also just be a way to create a Protocol + from a class? + + This is icky but it works - revisit in future. + """ + if t.TYPE_CHECKING: + return baseclass # pragma: no cover + return object # type: ignore + + +class StaticEngine(with_typehint(BaseEngine)): # type: ignore + @abstractmethod + def select_templates( + self, selector: str, first_loader: bool = False, first_preference: bool = False + ) -> t.List[str]: + """ + Resolves a template selector into a list of template names from the + loaders configured on this backend engine. + + :param selector: The template selector + :param first_loader: If True, return only the set of template names + from the first loader that matches any part of the selector. By + default (False) any template name that matches the selector from + any loader will be returned. + :param first_preference: If true, return only the templates that match + the first preference for each loader. When combined with + first_loader will return only the first preference(s) of the first + loader. Preferences are loader specific and documented on the + loader. + :return: The list of resolved template names + """ + + @abstractmethod + def search_templates( + self, prefix: str, first_loader: bool = False + ) -> t.Generator[Template, None, None]: + """ + Resolves a partial template selector into a list of template names from the + loaders configured on this backend engine. + + :param prefix: The template prefix to search for + :param first_loader: Search only the first loader + :return: The list of resolved template names + """ diff --git a/render_static/backends/django.py b/render_static/backends/django.py new file mode 100755 index 0000000..64a03a5 --- /dev/null +++ b/render_static/backends/django.py @@ -0,0 +1,110 @@ +""" +Extensions of the standard Django template backends that add a few more +configuration parameters and functionality necessary for the static engine. +These backends should be used instead of the standard backends! +""" + +from typing import Dict, Generator, List + +from django.template import Template, TemplateDoesNotExist +from django.template.backends.django import DjangoTemplates + +from render_static.loaders.django import SearchableLoader +from render_static.loaders.mixins import BatchLoaderMixin + +from .base import StaticEngine + +__all__ = ["StaticDjangoTemplates"] + + +class StaticDjangoTemplates(StaticEngine, DjangoTemplates): + """ + Extend the standard ``django.template.backends.django.DjangoTemplates`` + backend to add options and change the default loaders. + + By default this backend will search for templates in application + directories named ``static_templates``. The ``app_dir`` option is added to + the standard options to allow users to override this location. + + :param params: The parameters as passed into the ``STATIC_TEMPLATES`` + configuration for this backend. + """ + + _app_dirname: str = "static_templates" + + @property + def app_dirname(self) -> str: + return self._app_dirname + + def __init__(self, params: Dict) -> None: + params = params.copy() + options = params.pop("OPTIONS").copy() + loaders = options.get("loaders", None) + options.setdefault("builtins", ["render_static.templatetags.render_static"]) + self._app_dirname = options.pop("app_dir", self.app_dirname) + if loaders is None: + loaders = ["render_static.loaders.StaticFilesystemLoader"] + if params.get("APP_DIRS", False): + loaders += ["render_static.loaders.StaticAppDirectoriesLoader"] + # base class with throw if this isn't present, it must be false + params["APP_DIRS"] = False + options["loaders"] = loaders + params["OPTIONS"] = options + super().__init__(params) + setattr(self.engine, "app_dirname", self.app_dirname) + + def select_templates( + self, selector: str, first_loader: bool = False, first_preference: bool = False + ) -> List[str]: + """ + Resolves a template selector into a list of template names from the + loaders configured on this backend engine. + + :param selector: The template selector + :param first_loader: If True, return only the set of template names + from the first loader that matches any part of the selector. By + default (False) any template name that matches the selector from + any loader will be returned. + :param first_preference: If true, return only the templates that match + the first preference for each loader. When combined with + first_loader will return only the first preference(s) of the first + loader. Preferences are loader specific and documented on the + loader. + :return: The list of resolved template names + """ + template_names = set() + for loader in self.engine.template_loaders: + try: + if isinstance(loader, BatchLoaderMixin): + for templates in loader.select_templates(selector): + for tmpl in templates: + template_names.add(tmpl) + if templates and first_preference: + break + else: + loader.get_template(selector) + template_names.add(selector) + if first_loader and template_names: + return list(template_names) + except TemplateDoesNotExist: + continue + if template_names: + return list(template_names) + raise TemplateDoesNotExist( + f"Template selector {selector} did not resolve to any template " f"names." + ) + + def search_templates( + self, prefix: str, first_loader: bool = False + ) -> Generator[Template, None, None]: + """ + Resolves a partial template selector into a list of template names from the + loaders configured on this backend engine. + + :param prefix: The template prefix to search for + :param first_loader: Search only the first loader + :return: The list of resolved template names + """ + for loader in self.engine.template_loaders[: 1 if first_loader else None]: + if isinstance(loader, SearchableLoader): + yield from loader.search(prefix) diff --git a/render_static/backends/jinja2.py b/render_static/backends/jinja2.py new file mode 100644 index 0000000..2bbdef2 --- /dev/null +++ b/render_static/backends/jinja2.py @@ -0,0 +1,158 @@ +from os.path import normpath +from pathlib import Path +from typing import Dict, Generator, List, Tuple, cast + +from django.apps import apps +from django.apps.config import AppConfig +from django.template import TemplateDoesNotExist +from django.template.backends.jinja2 import Jinja2, Template +from jinja2 import Environment + +from render_static.loaders.jinja2 import SearchableLoader as SearchableJinja2Loader +from render_static.loaders.jinja2 import StaticFileSystemBatchLoader +from render_static.loaders.mixins import BatchLoaderMixin +from render_static.origin import AppOrigin +from render_static.templatetags import render_static + +from .base import StaticEngine + +__all__ = ["StaticJinja2Templates"] + + +def default_env(**options): + """ + The default Jinja2 backend environment. This environment adds the tags + and filters from render_static. + + :param options: + :return: + """ + env = Environment(**options) + env.globals.update(render_static.register.filters) + env.globals.update( + { + name: getattr(tag, "__wrapped__", tag) + for name, tag in render_static.register.tags.items() + } + ) + return env + + +class StaticJinja2Templates(StaticEngine, Jinja2): + """ + Extend the standard ``django.template.backends.jinja2.Jinja2`` backend + to add options. Unlike with the standard backend, the loaders used for + this backend remain unchanged. + + By default this backend will search for templates in application + directories named ``static_jinja2``. The ``app_dir`` option is added to + the standard option to allow users to override this location. + + :param params: The parameters as passed into the ``STATIC_TEMPLATES`` + configuration for this backend. + """ + + _app_dirname = "static_jinja2" + + @property + def app_dirname(self) -> str: + return self._app_dirname + + app_directories: List[Tuple[Path, AppConfig]] = [] + + def __init__(self, params: Dict) -> None: + params = params.copy() + self.dirs = list(params.get("DIRS", [])) + self.app_dirs = params.get("APP_DIRS", False) + options = params.pop("OPTIONS").copy() + options.setdefault("environment", "render_static.backends.jinja2.default_env") + self._app_dirname = options.pop("app_dir", self.app_dirname) + + if "loader" not in options: + options["loader"] = StaticFileSystemBatchLoader(self.template_dirs) + + params["OPTIONS"] = options + + self.app_directories = [ + (Path(app_config.path) / self.app_dirname, app_config) + for app_config in apps.get_app_configs() + if app_config.path and (Path(app_config.path) / self.app_dirname).is_dir() + ] + + super().__init__(params) + + def get_template(self, template_name: str) -> Template: + """ + We override the Jinja2 get_template method so we can monkey patch + in the AppConfig of the origin if this template was from an app + directory. This information is used later down the line when + deciding where to write rendered templates. For the django template + backend we modified the loaders but modifying the Jinja2 loaders + would be much more invasive. + """ + template = cast(Template, super().get_template(template_name)) + + for app_dir, app in self.app_directories: + if normpath(template.origin.name).startswith(normpath(app_dir)): + template.origin = AppOrigin( # type: ignore + name=template.origin.name, + template_name=template.origin.template_name, + app=app, + ) + break + return template + + def select_templates( + self, + selector: str, + first_loader: bool = False, + first_preference: bool = False, + ) -> List[str]: + """ + Resolves a template selector into a list of template names from + the loader configured on this backend engine. + + :param selector: The template selector + :param first_loader: This is ignored for the Jinja2 engine. The + Jinja2 engine only has one loader. + :param first_preference: If true, return only the templates that + match the first preference for the loader. Preferences are + loader specific and documented on the loader. + :return: The list of resolved template names + """ + template_names = set() + if isinstance(self.env.loader, BatchLoaderMixin): + for templates in self.env.loader.select_templates(selector): + if templates: + for tmpl in templates: + template_names.add(tmpl) + if first_preference: + break + else: + self.get_template(selector) + template_names.add(selector) + + if template_names: + return list(template_names) + + raise TemplateDoesNotExist( + f"Template selector {selector} did not resolve to any " f"template names." + ) + + def search_templates( # type: ignore[override] + self, + prefix: str, + first_loader: bool = False, + ) -> Generator[Template, None, None]: + """ + Resolves a partial template selector into a list of template names from the + loaders configured on this backend engine. + + :param prefix: The template prefix to search for + :param first_loader: This is ignored for the Jinja2 engine because there is + only one loader + :return: The list of resolved template names + """ + if isinstance(self.env.loader, SearchableJinja2Loader): + for tmpl in self.env.loader.search(self.env, prefix): + yield Template(tmpl, self) diff --git a/render_static/context.py b/render_static/context.py index ab56393..27a374e 100644 --- a/render_static/context.py +++ b/render_static/context.py @@ -7,7 +7,9 @@ import json import pickle import re +from importlib import import_module from pathlib import Path +from types import ModuleType from typing import Callable, Dict, Optional, Sequence, Tuple, Union from django.utils.module_loading import import_string @@ -33,7 +35,9 @@ def yaml_load(*args, **kwargs): # type: ignore _import_regex = re.compile(r"^[\w]+([.][\w]+)*$") -def resolve_context(context: Optional[Union[Dict, str, Path, Callable]]) -> Dict: +def resolve_context( + context: Optional[Union[Dict, str, Path, Callable, ModuleType]], +) -> Dict: """ Resolve the context specifier into a context dictionary. Context specifier may be a packaged resource, a path-like object or a string path to a json @@ -58,6 +62,8 @@ def resolve_context(context: Optional[Union[Dict, str, Path, Callable]]) -> Dict return context if getattr(context, "module_not_found", False): raise InvalidContext("Unable to locate resource context!") + if isinstance(context, ModuleType): + return {k: v for k, v in vars(context).items() if not k.startswith("_")} context = str(context) for try_load, can_load in _loader_try_order(context): try: @@ -79,7 +85,7 @@ def _from_json(file_path: str, throw: bool = True) -> Optional[Dict]: try: with open(file_path, "rb") as ctx_f: return json.load(ctx_f) - except Exception as err: # pylint: disable=W0703 + except Exception as err: if throw: raise err return None @@ -95,7 +101,7 @@ def _from_yaml(file_path: str, throw: bool = True) -> Optional[Dict]: try: with open(file_path, "rb") as ctx_f: return yaml_load(ctx_f, Loader=FullLoader) - except Exception as err: # pylint: disable=W0703 + except Exception as err: if throw: raise err return None @@ -113,7 +119,7 @@ def _from_pickle(file_path: str, throw: bool = True) -> Optional[Dict]: ctx = pickle.load(ctx_f) if isinstance(ctx, dict): return ctx - except Exception as err: # pylint: disable=W0703 + except Exception as err: if throw: raise err return None @@ -131,9 +137,9 @@ def _from_python(file_path: str, throw: bool = True) -> Optional[Dict]: try: with open(file_path, "rb") as ctx_f: compiled_code = compile(ctx_f.read(), file_path, "exec") - exec(compiled_code, {}, ctx) # pylint: disable=W0122 + exec(compiled_code, {}, ctx) return ctx - except Exception as err: # pylint: disable=W0703 + except Exception as err: if throw: raise err return None @@ -143,17 +149,23 @@ def _from_import(import_path: str, throw: bool = True) -> Optional[Dict]: """ Attempt to load context as from an import string. - :param file_path: The path to the pickled file + :param import_path: The import path to load, may point to a callable that + returns a context, a dictionary or a module :param throw: If true, let any exceptions propagate out :return: A dictionary or None if the context was not a pickled dictionary. """ try: - context = import_string(import_path) + try: + context = import_string(import_path) + except ImportError: + context = import_module(import_path) if callable(context): context = context() if isinstance(context, dict): return context - except Exception as err: # pylint: disable=W0703 + if isinstance(context, ModuleType): + return {k: v for k, v in vars(context).items() if not k.startswith("_")} + except Exception as err: if throw: raise err return None diff --git a/render_static/engine.py b/render_static/engine.py index 19fcb71..308d6a5 100644 --- a/render_static/engine.py +++ b/render_static/engine.py @@ -1,9 +1,7 @@ -# pylint: disable=C0114 - import os from collections import Counter, namedtuple from pathlib import Path -from typing import Callable, Dict, Generator, List, Optional, Tuple, Union +from typing import Callable, Dict, Generator, List, Optional, Tuple, Union, cast from django.conf import settings from django.core.exceptions import ImproperlyConfigured @@ -14,17 +12,10 @@ from django.utils.functional import cached_property from django.utils.module_loading import import_string -from render_static import Jinja2DependencyNeeded -from render_static.backends import StaticDjangoTemplates, StaticJinja2Templates +from render_static.backends.base import StaticEngine from render_static.context import resolve_context from render_static.exceptions import InvalidContext -try: - # pylint: disable=C0412 - from django.template.backends.jinja2 import Template as Jinja2Template -except ImportError: - Jinja2Template = Jinja2DependencyNeeded - __all__ = ["StaticTemplateEngine", "Render"] @@ -105,7 +96,7 @@ class StaticTemplateEngine: # This engine uses a custom configuration engine = StaticTemplateEngine({ 'ENGINES': [{ - 'BACKEND': 'render_static.backends.StaticJinja2Templates', + 'BACKEND': 'render_static.backends.jinja2.StaticJinja2Templates', 'APP_DIRS': True }], 'context': { @@ -142,6 +133,8 @@ class StaticTemplateEngine: passed in or specified in settings. """ + app_dirname: str + config_: Dict = {} DEFAULT_ENGINE_CONFIG = [ @@ -378,9 +371,7 @@ def engines(self) -> dict: return engines - def __getitem__( - self, alias: str - ) -> Union[StaticDjangoTemplates, StaticJinja2Templates]: + def __getitem__(self, alias: str) -> StaticEngine: """ Accessor for backend instances indexed by name. @@ -402,7 +393,7 @@ def __iter__(self): """ return iter(self.engines) - def all(self) -> List[Union[StaticDjangoTemplates, StaticJinja2Templates]]: + def all(self) -> List[StaticEngine]: """ Get a list of all registered engines in order of precedence. :return: A list of engine instances in order of precedence @@ -412,7 +403,7 @@ def all(self) -> List[Union[StaticDjangoTemplates, StaticJinja2Templates]]: @staticmethod def resolve_destination( config: TemplateConfig, - template: Union[Jinja2Template, DjangoTemplate], + template: DjangoTemplate, batch: bool, dest: Optional[Union[str, Path]] = None, ) -> Path: @@ -448,13 +439,13 @@ def resolve_destination( f"an app!" ) from err - dest /= template.template.name + dest /= template.template.name or "" elif batch or Path(dest).is_dir(): - dest = Path(dest) / template.template.name + dest = Path(dest) / (template.template.name or "") return Path(dest if dest else "") - def render_to_disk( # pylint: disable=R0913 + def render_to_disk( self, selector: str, context: Optional[Dict] = None, @@ -494,7 +485,7 @@ def render_to_disk( # pylint: disable=R0913 :raises ImproperlyConfigured: if not enough information was given to render and write the template """ - return [ # pylint: disable=R1721 + return [ render for render in self.render_each( selector, @@ -506,7 +497,7 @@ def render_to_disk( # pylint: disable=R0913 ) ] - def find( # pylint: disable=R0914 + def find( self, *selectors: str, dest: Optional[Union[str, Path]] = None, @@ -546,12 +537,12 @@ def find( # pylint: disable=R0914 first_preference=first_preference, ) - def search( # pylint: disable=R0914 + def search( self, prefix: str, first_engine: bool = False, first_loader: bool = False, - ) -> Generator[Union[Template, Jinja2Template], None, None]: + ) -> Generator[Template, None, None]: """ Search for all templates that match the given selectors and yield Render objects for each one. @@ -567,7 +558,7 @@ def search( # pylint: disable=R0914 for engine in self.all()[0 : 1 if first_engine else None]: yield from engine.search_templates(prefix, first_loader=first_loader) - def render_each( # pylint: disable=R0914 + def render_each( self, *selectors: str, context: Optional[Union[Dict, str, Path, Callable]] = None, @@ -649,7 +640,7 @@ def resolve_renderings( :param kwargs: Pass through parameters from render_each :yield: Render objects """ - templates: Dict[str, Union[DjangoTemplate, Jinja2Template]] = {} + templates: Dict[str, DjangoTemplate] = {} chain = [] for engine in self.all(): try: @@ -660,7 +651,11 @@ def resolve_renderings( ): try: templates.setdefault( - template_name, engine.get_template(template_name) + template_name, + cast( + DjangoTemplate, + engine.get_template(template_name), + ), ) except TemplateDoesNotExist as tdne: # pragma: no cover # this should be impossible w/o a loader bug! diff --git a/render_static/loaders/__init__.py b/render_static/loaders/__init__.py index 57ae3f2..4ecb468 100644 --- a/render_static/loaders/__init__.py +++ b/render_static/loaders/__init__.py @@ -1,5 +1,3 @@ -# pylint: disable=C0114 - from render_static.loaders.django import ( StaticAppDirectoriesBatchLoader, StaticAppDirectoriesLoader, diff --git a/render_static/loaders/django.py b/render_static/loaders/django.py index c1a07f1..b7b0edb 100644 --- a/render_static/loaders/django.py +++ b/render_static/loaders/django.py @@ -6,19 +6,24 @@ Templates in the future. """ -import os -from collections.abc import Container -from glob import glob from pathlib import Path -from typing import Generator, List, Optional, Protocol, Tuple, Union, runtime_checkable +from typing import ( + Generator, + List, + Optional, + Protocol, + Tuple, + Union, + runtime_checkable, +) from django.apps import apps from django.apps.config import AppConfig from django.core.exceptions import SuspiciousFileOperation -from django.template import Template, TemplateDoesNotExist +from django.template import Origin, Template, TemplateDoesNotExist from django.template.loaders.app_directories import Loader as AppDirLoader from django.template.loaders.filesystem import Loader as FilesystemLoader -from django.template.loaders.filesystem import safe_join +from django.template.loaders.filesystem import safe_join # type: ignore[attr-defined] from django.template.loaders.locmem import Loader as LocMemLoader from render_static.loaders.mixins import BatchLoaderMixin @@ -40,12 +45,13 @@ class SearchableLoader(Protocol): Loaders should implement this protocol to support shell tab-completion. """ - def search(self, selector: str) -> Generator[Union[Template], None, None]: + def search(self, selector: str) -> Generator[Template, None, None]: """ Search for templates matching the selector pattern. :param selector: A glob pattern, or file name """ + ... # pragma: no cover class DirectorySupport(FilesystemLoader): @@ -58,7 +64,7 @@ class DirectorySupport(FilesystemLoader): is_dir = False def get_template( - self, template_name: str, skip: Optional[Container] = None + self, template_name: str, skip: Optional[List[Origin]] = None ) -> Template: """ Wrap the super class's get_template method and set our is_dir @@ -71,10 +77,10 @@ def get_template( """ self.is_dir = False template = super().get_template(template_name, skip=skip) - template.is_dir = self.is_dir + setattr(template, "is_dir", self.is_dir) return template - def get_contents(self, origin: AppOrigin) -> str: + def get_contents(self, origin: Origin) -> str: """ We wrap the super class's get_contents implementation and set the is_dir flag if the origin is a directory. This is @@ -92,7 +98,7 @@ def get_contents(self, origin: AppOrigin) -> str: self.is_dir = True return "" - def search(self, prefix: str) -> Generator[Union[Template], None, None]: + def search(self, prefix: str) -> Generator[Template, None, None]: """ Return all Template objects at paths that start with the given path prefix. @@ -101,9 +107,8 @@ def search(self, prefix: str) -> Generator[Union[Template], None, None]: :yield: All Template objects that have names that start with the prefix """ prefix = str(Path(prefix)) if prefix else "" # normalize! - for template_dir, _ in self.get_dirs(): - template_dir = Path(template_dir) - for path in walk(template_dir): + for template_dir in self.get_dirs(): + for path in walk(Path(template_dir)): if not str(path).startswith(prefix): continue try: @@ -133,7 +138,7 @@ class StaticLocMemLoader(LocMemLoader): Simple extension of ``django.template.loaders.locmem.Loader`` """ - def search(self, prefix: str) -> Generator[Union[Template], None, None]: + def search(self, prefix: str) -> Generator[Template, None, None]: """ Search for templates matching the selector pattern. @@ -156,24 +161,27 @@ class StaticAppDirectoriesLoader(DirectorySupport, AppDirLoader): the template should be rendered to disk. """ - def get_dirs(self) -> Tuple[Tuple[Path, AppConfig], ...]: + def get_dirs(self) -> List[Union[str, Path]]: + return [pth for pth, _ in self.get_app_dirs()] + + def get_app_dirs(self) -> List[Tuple[Union[str, Path], AppConfig]]: """ Fetch the directories :return: """ template_dirs = [ - (Path(app_config.path) / self.engine.app_dirname, app_config) + (Path(app_config.path) / getattr(self.engine, "app_dirname"), app_config) for app_config in apps.get_app_configs() if ( app_config.path - and (Path(app_config.path) / self.engine.app_dirname).is_dir() + and ( + Path(app_config.path) / getattr(self.engine, "app_dirname") + ).is_dir() ) ] - return tuple(template_dirs) + return template_dirs - def get_template_sources( - self, template_name: str - ) -> Generator[Union[AppOrigin, List[AppOrigin]], None, None]: + def get_template_sources(self, template_name: str) -> Generator[Origin, None, None]: """ Yield the origins of all the templates from apps that match the given template name. @@ -181,7 +189,7 @@ def get_template_sources( :param template_name: The name of the template to resolve :return: Yielded AppOrigins for the found templates. """ - for template_dir, app_config in self.get_dirs(): + for template_dir, app_config in self.get_app_dirs(): try: name = safe_join(template_dir, template_name) except SuspiciousFileOperation: @@ -194,7 +202,7 @@ def get_template_sources( ) -class StaticAppDirectoriesBatchLoader(StaticAppDirectoriesLoader): +class StaticAppDirectoriesBatchLoader(StaticAppDirectoriesLoader, BatchLoaderMixin): """ A loader that enables glob pattern selectors to load batches of templates from app directories. @@ -202,22 +210,3 @@ class StaticAppDirectoriesBatchLoader(StaticAppDirectoriesLoader): Yields batches of template names in order of preference, where preference is defined by the precedence of Django apps. """ - - def select_templates(self, selector: str) -> Generator[List[str], None, None]: - """ - Yields template names matching the selector pattern. - - :param selector: A glob pattern, or file name - """ - for template_dir, app_config in self.get_dirs(): # pylint: disable=W0612 - try: - pattern = safe_join(template_dir, selector) - except SuspiciousFileOperation: - # The joined path was located outside of this template_dir - # (it might be inside another one, so this isn't fatal). - continue - - yield [ - os.path.relpath(str(file), str(template_dir)) - for file in glob(pattern, recursive=True) - ] diff --git a/render_static/loaders/jinja2.py b/render_static/loaders/jinja2.py index 7e78cc1..be7730d 100644 --- a/render_static/loaders/jinja2.py +++ b/render_static/loaders/jinja2.py @@ -7,6 +7,7 @@ https://jinja.palletsprojects.com/en/3.0.x/api/#loaders """ + from os.path import normpath from pathlib import Path from typing import ( @@ -14,39 +15,30 @@ Any, Callable, Generator, + List, MutableMapping, Optional, Tuple, + Union, +) + +from jinja2.exceptions import TemplateNotFound +from jinja2.loaders import ( + BaseLoader, + ChoiceLoader, + DictLoader, + FileSystemLoader, + FunctionLoader, + ModuleLoader, + PackageLoader, + PrefixLoader, ) -from render_static import Jinja2DependencyNeeded from render_static.loaders.mixins import BatchLoaderMixin -try: - from jinja2.exceptions import TemplateNotFound - from jinja2.loaders import ( - BaseLoader, - ChoiceLoader, - DictLoader, - FileSystemLoader, - FunctionLoader, - ModuleLoader, - PackageLoader, - PrefixLoader, - ) - - if TYPE_CHECKING: # pragma: no cover - from jinja2 import Environment, Template - -except ImportError: - ChoiceLoader = Jinja2DependencyNeeded # type: ignore - DictLoader = Jinja2DependencyNeeded # type: ignore - FileSystemLoader = Jinja2DependencyNeeded # type: ignore - FunctionLoader = Jinja2DependencyNeeded # type: ignore - ModuleLoader = Jinja2DependencyNeeded # type: ignore - PackageLoader = Jinja2DependencyNeeded # type: ignore - PrefixLoader = Jinja2DependencyNeeded # type: ignore - BaseLoader = Jinja2DependencyNeeded # type: ignore +if TYPE_CHECKING: # pragma: no cover + from jinja2 import Environment, Template + __all__ = [ "StaticFileSystemLoader", @@ -85,9 +77,7 @@ def search( pass -class StaticFileSystemLoader( - SearchableLoader, FileSystemLoader -): # pylint: disable=R0903 +class StaticFileSystemLoader(SearchableLoader, FileSystemLoader): """ https://jinja.palletsprojects.com/en/3.0.x/api/#jinja2.FileSystemLoader @@ -100,9 +90,7 @@ def load( self, environment: "Environment", name: str, - globals: Optional[ # pylint: disable=redefined-builtin - MutableMapping[str, Any] - ] = None, + globals: Optional[MutableMapping[str, Any]] = None, ) -> "Template": """ Wrap load so we can tag directory templates with is_dir. @@ -126,8 +114,7 @@ def get_source( pth = Path(search_path) / template if pth.is_dir(): self.is_dir = True - # code cov bug here, ignore it - return ("", normpath(pth), lambda: True) + return ("", normpath(pth), lambda: True) # pragma: no cover raise @@ -142,41 +129,41 @@ class StaticFileSystemBatchLoader(StaticFileSystemLoader, BatchLoaderMixin): specified. """ - def get_dirs(self): - return self.searchpath + def get_dirs(self) -> List[Union[str, Path]]: + return self.searchpath # type: ignore -class StaticPackageLoader(SearchableLoader, PackageLoader): # pylint: disable=R0903 +class StaticPackageLoader(SearchableLoader, PackageLoader): """ https://jinja.palletsprojects.com/en/3.0.x/api/#jinja2.PackageLoader """ -class StaticPrefixLoader(SearchableLoader, PrefixLoader): # pylint: disable=R0903 +class StaticPrefixLoader(SearchableLoader, PrefixLoader): """ https://jinja.palletsprojects.com/en/3.0.x/api/#jinja2.PrefixLoader """ -class StaticFunctionLoader(FunctionLoader): # pylint: disable=R0903 +class StaticFunctionLoader(FunctionLoader): """ https://jinja.palletsprojects.com/en/3.0.x/api/#jinja2.FunctionLoader """ -class StaticDictLoader(SearchableLoader, DictLoader): # pylint: disable=R0903 +class StaticDictLoader(SearchableLoader, DictLoader): """ https://jinja.palletsprojects.com/en/3.0.x/api/#jinja2.DictLoader """ -class StaticChoiceLoader(SearchableLoader, ChoiceLoader): # pylint: disable=R0903 +class StaticChoiceLoader(SearchableLoader, ChoiceLoader): """ https://jinja.palletsprojects.com/en/3.0.x/api/#jinja2.ChoiceLoader """ -class StaticModuleLoader(ModuleLoader): # pylint: disable=R0903 +class StaticModuleLoader(ModuleLoader): """ https://jinja.palletsprojects.com/en/3.0.x/api/#jinja2.ModuleLoader """ diff --git a/render_static/loaders/mixins.py b/render_static/loaders/mixins.py index 9494e07..88eb2e4 100644 --- a/render_static/loaders/mixins.py +++ b/render_static/loaders/mixins.py @@ -1,12 +1,14 @@ """ Helper classes for augmenting loader behavior. """ + from glob import glob from os.path import relpath -from typing import Generator, List +from pathlib import Path +from typing import Generator, List, Union from django.core.exceptions import SuspiciousFileOperation -from django.template.loaders.filesystem import safe_join +from django.template.loaders.filesystem import safe_join # type: ignore[attr-defined] __all__ = ["BatchLoaderMixin"] @@ -20,7 +22,7 @@ class BatchLoaderMixin: is defined by the order directories are listed in. """ - def get_dirs(self) -> List[str]: + def get_dirs(self) -> List[Union[str, Path]]: """ Return a priority ordered list of directories on the search path of this loader. diff --git a/render_static/management/commands/renderstatic.py b/render_static/management/commands/renderstatic.py index 36af190..221d284 100755 --- a/render_static/management/commands/renderstatic.py +++ b/render_static/management/commands/renderstatic.py @@ -10,6 +10,7 @@ ``STATIC_TEMPLATES`` for it to be found and rendered. Such templates will be given the global context as specified in ``STATIC_TEMPLATES``. """ + import sys import typing as t from pathlib import Path @@ -17,8 +18,14 @@ from click import Context, Parameter from click.shell_completion import CompletionItem from django.core.management.base import CommandError -from django.utils.translation import gettext_lazy as _ -from django_typer import TyperCommand +from django.utils.translation import gettext as _ +from django_typer.completers import ( + chain, + complete_directory, + complete_import_path, + complete_path, +) +from django_typer.management import TyperCommand from typer import Argument, Option from render_static.engine import StaticTemplateEngine @@ -43,23 +50,22 @@ def complete_selector( first_engine=bool(ctx.params.get("first_engine")), first_loader=bool(ctx.params.get("first_loader")), ): - if template.name not in present and template.name not in completions: + tmpl_name = str(template.origin.template_name or "") + if tmpl_name and tmpl_name not in present and tmpl_name not in completions: # the slicing is because we need to denormalize the prefix if the # search process normalized the name somehow, because the prefixes # must exactly match whats on the command line for most shell completion # utilities completions.append( - CompletionItem(f"{incomplete}{template.name[len(incomplete):]}") + CompletionItem(f"{incomplete}{tmpl_name[len(incomplete):]}") ) return completions class Command(TyperCommand): - # pylint: disable=C0115 - help = _("Generate static files from static templates.") - def handle( # pylint: disable=W0221 + def handle( self, selectors: Annotated[ t.Optional[t.List[str]], @@ -76,7 +82,7 @@ def handle( # pylint: disable=W0221 ), ] = None, context: Annotated[ - t.Optional[Path], + t.Optional[str], Option( "--context", "-c", @@ -88,6 +94,7 @@ def handle( # pylint: disable=W0221 "python files, json files, yaml files, or pickled python " "dictionaries.", ), + shell_complete=chain(complete_path, complete_import_path), ), ] = None, destination: Annotated[ @@ -101,6 +108,7 @@ def handle( # pylint: disable=W0221 "one exists. If no destination is specified in settings or " "here, the default destination is settings.STATIC_ROOT." ), + shell_complete=complete_directory, ), ] = None, first_engine: Annotated[ @@ -151,9 +159,7 @@ def handle( # pylint: disable=W0221 if not selectors: self.stdout.write( - self.style.WARNING( # pylint: disable=E1101 - "No templates selected for generation." - ) + self.style.WARNING(_("No templates selected for generation.")) ) return @@ -167,7 +173,9 @@ def handle( # pylint: disable=W0221 first_preference=first_preference, ): self.stdout.write( - self.style.SUCCESS(f"Rendered {render}.") # pylint: disable=E1101 + self.style.SUCCESS(_("Rendered {render}.").format(render=render)) ) except Exception as exp: - raise CommandError(f"Error rendering template to disk: {exp}") from exp + raise CommandError( + _("Error rendering template to disk: {exp}").format(exp=exp) + ) from exp diff --git a/render_static/origin.py b/render_static/origin.py index 628f506..8e1edda 100644 --- a/render_static/origin.py +++ b/render_static/origin.py @@ -1,10 +1,7 @@ -# pylint: disable=C0114 +from typing import Optional -from typing import Union - -from django.apps.config import AppConfig +from django.apps import AppConfig from django.template import Origin -from django.template.loaders.base import Loader __all__ = ["AppOrigin"] @@ -19,11 +16,15 @@ class AppOrigin(Origin): :param kwargs: """ - def __init__(self, *args: str, **kwargs: Union[str, Loader, AppConfig]) -> None: - self.app = kwargs.pop("app", None) - super().__init__(*args, **kwargs) + app: Optional[AppConfig] = None + + def __init__( + self, name, app: Optional[AppConfig] = None, template_name=None, loader=None + ) -> None: + self.app = app + super().__init__(name, template_name=template_name, loader=loader) - def __eq__(self, other: Union[Origin, "AppOrigin"]) -> bool: + def __eq__(self, other) -> bool: """ Determine origin equality as defined by template name and application origin. AppOrigins will compare as equal to Origins if neither have an diff --git a/render_static/resource.py b/render_static/resource.py index a701bdb..58bdbd5 100644 --- a/render_static/resource.py +++ b/render_static/resource.py @@ -5,6 +5,7 @@ resource('package.module', 'resource.file') """ + import contextlib import sys import types @@ -15,14 +16,14 @@ # Distinguishing between different versions of Python: if sys.version_info >= (3, 9): - from importlib.resources import as_file, files # pylint: disable=E0611 + from importlib.resources import as_file, files else: try: from importlib_resources import as_file, files except ImportError: @singledispatch - def need_install(*args, **kwargs): + def need_install(*args, **kwargs): # pragma: no cover """ On platforms <3.9, the importlib_resources backport needs to be available to use resources. diff --git a/render_static/templatetags/render_static.py b/render_static/templatetags/render_static.py index fccb759..172a860 100755 --- a/render_static/templatetags/render_static.py +++ b/render_static/templatetags/render_static.py @@ -18,6 +18,7 @@ Optional, Type, Union, + cast, ) from django import template @@ -28,7 +29,11 @@ from django.utils.module_loading import import_string from django.utils.safestring import SafeString -from render_static.transpilers import Transpiler, TranspilerTarget, TranspilerTargets +from render_static.transpilers.base import ( + Transpiler, + TranspilerTarget, + TranspilerTargets, +) from render_static.transpilers.defines_to_js import DefaultDefineTranspiler from render_static.transpilers.enums_to_js import EnumClassWriter from render_static.transpilers.urls_to_js import ClassURLWriter @@ -56,10 +61,10 @@ class to use. if isinstance(targets, (type, str)) or not isinstance(targets, Collection): targets = [targets] - transpiler = ( + transpiler_cls = ( import_string(transpiler) if isinstance(transpiler, str) else transpiler ) - return SafeString(transpiler(**kwargs).transpile(targets)) + return SafeString(transpiler_cls(**kwargs).transpile(targets)) class OverrideNode(Node): @@ -70,7 +75,11 @@ class OverrideNode(Node): :param nodelist: The child nodes for this node. Should be empty """ - def __init__(self, override_name: Optional[str], nodelist: NodeList): + def __init__( + self, + override_name: Optional[Union[str, template.base.FilterExpression]], + nodelist: NodeList, + ): self.override_name = override_name or f"_{id(self)}" self.nodelist = nodelist self.context = Context() @@ -147,7 +156,8 @@ def get_resolved_arguments(self, context: Context) -> Dict[str, Any]: overrides = self.get_nodes_by_type(OverrideNode) if overrides: resolved_kwargs["overrides"] = { - override.bind(context): override for override in overrides + cast(OverrideNode, override).bind(context): override + for override in overrides } return resolved_kwargs @@ -168,7 +178,7 @@ def transpiler_tag( func: Optional[Callable] = None, targets: Union[int, str] = 0, name: Optional[str] = None, - node: Type[Node] = TranspilerNode, + node: Type[TranspilerNode] = TranspilerNode, ): """ Register a callable as a transpiler tag. This decorator is similar @@ -286,7 +296,7 @@ class to use. @transpiler_tag(targets="url_conf") -def urls_to_js( # pylint: disable=R0913,R0915 +def urls_to_js( transpiler: TranspilerType = ClassURLWriter, url_conf: Optional[Union[ModuleType, str]] = None, indent: str = "\t", @@ -486,8 +496,8 @@ def override(parser, token): parser, token.split_contents()[1:], ["override"], - [], - [], + None, + None, [], [], {}, diff --git a/render_static/tests/chain/urls.py b/render_static/tests/chain/urls.py deleted file mode 100644 index a47d814..0000000 --- a/render_static/tests/chain/urls.py +++ /dev/null @@ -1,13 +0,0 @@ -from django.urls import include, path, re_path - -app_name = "chain" - -urlpatterns = [ - path( - "chain//", include("render_static.tests.spa.urls", namespace="spa") - ), - re_path( - r"^chain/(?P\w+)/", - include("render_static.tests.spa.urls", namespace="spa_re"), - ), -] diff --git a/render_static/tests/examples/static_templates/examples/defines.js b/render_static/tests/examples/static_templates/examples/defines.js deleted file mode 100644 index f9ac519..0000000 --- a/render_static/tests/examples/static_templates/examples/defines.js +++ /dev/null @@ -1,2 +0,0 @@ -{% defines_to_js defines="render_static.tests.examples.models" %} -console.log(JSON.stringify(defines)); diff --git a/render_static/tests/jinja2_tests.py b/render_static/tests/jinja2_tests.py deleted file mode 100644 index e25822e..0000000 --- a/render_static/tests/jinja2_tests.py +++ /dev/null @@ -1,8 +0,0 @@ -try: - # weird issue where cant just import jinja2 b/c leftover __pycache__ - # allows it to "import" - from jinja2 import environment - - from render_static.tests._jinja2_tests import * -except ImportError: - pass diff --git a/render_static/tests/spa/static_templates/spa/urls.js b/render_static/tests/spa/static_templates/spa/urls.js deleted file mode 100644 index 3c0b4ab..0000000 --- a/render_static/tests/spa/static_templates/spa/urls.js +++ /dev/null @@ -1 +0,0 @@ -{% urls_to_js visitor="render_static.ClassURLWriter" es5=es5|default:False include=include %} diff --git a/render_static/tests/spa_urls.py b/render_static/tests/spa_urls.py deleted file mode 100644 index e8e7dc0..0000000 --- a/render_static/tests/spa_urls.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.urls import include, path - -urlpatterns = [ - path("spa1/", include("render_static.tests.spa.urls", namespace="spa1")), - path("spa2/", include("render_static.tests.spa.urls", namespace="spa2")), -] diff --git a/render_static/tests/urls_bug_13.py b/render_static/tests/urls_bug_13.py deleted file mode 100644 index 036bafc..0000000 --- a/render_static/tests/urls_bug_13.py +++ /dev/null @@ -1,20 +0,0 @@ -""" -Reproduce: https://github.com/bckohan/django-render-static/issues/65 -""" -from django.urls import include, path, re_path - -urlpatterns = [ - path( - "spa1//", include("render_static.tests.spa.urls", namespace="spa1") - ), - path("spa2/", include("render_static.tests.spa.urls", namespace="spa2")), - path("multi//", include("render_static.tests.chain.urls")), - re_path( - r"^multi/(?P\w+)/", - include("render_static.tests.chain.urls", namespace="chain_re"), - ), - path( - "noslash/", - include("render_static.tests.chain.urls", namespace="noslash"), - ), -] diff --git a/render_static/tests/web_tests.py b/render_static/tests/web_tests.py deleted file mode 100644 index 8903243..0000000 --- a/render_static/tests/web_tests.py +++ /dev/null @@ -1,100 +0,0 @@ -""" -Single Page Application (SPA) tests. These tests test several patterns that -allow apps using urls_to_js to be included more than once and under different -namespaces. These patterns incur a dependency on renderstatic to any -users of the SPA apps. See Runtimes in the documentation for more details. -""" -import json -import logging -import os -import shutil - -import pytest -from django.core.management import call_command -from django.test import LiveServerTestCase, override_settings -from django.urls import reverse -from selenium import webdriver -from selenium.webdriver.chrome.options import Options -from selenium.webdriver.chrome.service import Service -from selenium.webdriver.common.by import By - -from render_static.tests.tests import LOCAL_STATIC_DIR, BaseTestCase - -logger = logging.getLogger(__name__) - - -if not shutil.which("chromedriver"): # pragma: no cover - pytest.skip( - "JavaScript tests require node.js to be installed.", allow_module_level=True - ) -else: - logger.info("Using chromedriver: %s", shutil.which("chromedriver")) - chrome_options = Options() - options = [ - "--headless", - "--disable-gpu", - "--window-size=1920,1200", - "--ignore-certificate-errors", - "--disable-extensions", - "--no-sandbox", - "--disable-dev-shm-usage", - "--start-maximized", - "--proxy-server='direct://'", - "--proxy-bypass-list=*", - ] - for option in options: - chrome_options.add_argument(option) - driver = webdriver.Chrome( - service=Service(shutil.which("chromedriver")), options=chrome_options - ) - - -@override_settings( - INSTALLED_APPS=[ - "render_static.tests.spa", - "render_static", - "django.contrib.auth", - "django.contrib.contenttypes", - "django.contrib.sessions", - "django.contrib.sites", - "django.contrib.messages", - "django.contrib.staticfiles", - "django.contrib.admin", - ], - ROOT_URLCONF="render_static.tests.spa_urls", - STATICFILES_DIRS=[ - ("spa", LOCAL_STATIC_DIR), - ], - STATIC_TEMPLATES={ - "templates": { - "spa/urls.js": { - "context": {"include": ["spa1", "spa2"]}, - "dest": str(LOCAL_STATIC_DIR / "urls.js"), - } - } - }, -) -class TestMultipleURLTreeSPAExample(BaseTestCase, LiveServerTestCase): - def setUp(self): - os.makedirs(LOCAL_STATIC_DIR, exist_ok=True) - call_command("renderstatic", "spa/urls.js", "--traceback") - call_command("collectstatic") - - def test_example_pattern(self): - driver.get(f'{self.live_server_url}{reverse("spa1:index")}') - from pprint import pprint - - pprint(driver.get_log("browser")) - elem = driver.find_element(By.ID, "qry-result") - self.assertEqual(json.loads(elem.text)["request"], "/spa1/qry/") - elem = driver.find_element(By.ID, "qry-result-arg") - self.assertEqual(json.loads(elem.text)["request"], "/spa1/qry/5") - - driver.get(f'{self.live_server_url}{reverse("spa2:index")}') - elem = driver.find_element(By.ID, "qry-result") - self.assertEqual(json.loads(elem.text)["request"], "/spa2/qry/") - elem = driver.find_element(By.ID, "qry-result-arg") - self.assertEqual(json.loads(elem.text)["request"], "/spa2/qry/5") - - # def tearDown(self): - # pass diff --git a/render_static/transpilers/__init__.py b/render_static/transpilers/__init__.py index 9f3da41..1917edf 100644 --- a/render_static/transpilers/__init__.py +++ b/render_static/transpilers/__init__.py @@ -1,564 +1,13 @@ -""" -Base transpiler components. -""" - -import json -import numbers -from abc import ABCMeta, abstractmethod -from collections.abc import Hashable -from datetime import date, datetime -from enum import Enum -from types import ModuleType -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Collection, - Dict, - Generator, - List, - Optional, - Set, - Type, - Union, -) -from warnings import warn - -from django.apps import apps -from django.apps.config import AppConfig -from django.utils.module_loading import import_module, import_string -from django.utils.safestring import SafeString - -if TYPE_CHECKING: # pragma: no cover - from render_static.templatetags.render_static import OverrideNode - +from .base import to_js, to_js_datetime +from .defines_to_js import DefaultDefineTranspiler +from .enums_to_js import EnumClassWriter +from .urls_to_js import ClassURLWriter, SimpleURLWriter __all__ = [ + "ClassURLWriter", + "SimpleURLWriter", + "EnumClassWriter", + "DefaultDefineTranspiler", "to_js", "to_js_datetime", - "CodeWriter", - "Transpiler", - "TranspilerTargets", - "TranspilerTarget", - "ResolvedTranspilerTarget", ] - -ResolvedTranspilerTarget = Union[Type[Any], ModuleType, AppConfig] -TranspilerTarget = Union[ResolvedTranspilerTarget, str] -TranspilerTargets = Collection[TranspilerTarget] - - -def to_js(value: Any) -> str: - """ - Default javascript transpilation function for values. Simply adds quotes - if it's a string and falls back on json.dumps() for non-strings and non- - numerics. - - :param value: The value to transpile - :return: Valid javascript code that represents the value - """ - if isinstance(value, Enum): - value = value.value - if isinstance(value, numbers.Number): - return str(value) - if isinstance(value, str): - return f'"{value}"' - try: - return json.dumps(value) - except TypeError: - if isinstance(value, datetime): - return f'"{value.isoformat()}"' - return f'"{str(value)}"' - - -def to_js_datetime(value: Any) -> str: - """ - A javascript value transpilation function that transpiles python dates and - datetimes to javascript Date objects instead of strings. To use this - function in any of the transpilation routines pass it to the to_javascript - parameter on any of the template tags:: - - {% ... to_javascript="render_static.transpilers.to_js_datetime" %} - - :param value: The value to transpile - :return: Valid javascript code that represents the value - """ - if isinstance(value, date): - return f'new Date("{value.isoformat()}")' - return to_js(value) - - -class _TargetTreeNode: - """ - Simple tree node for tracking python target hierarchy. - - :param target: The target at this node - """ - - target: Optional[TranspilerTarget] - children: List["_TargetTreeNode"] - transpile = False - - def __init__( - self, target: Optional[TranspilerTarget] = None, transpile: bool = False - ): - self.target = target - self.children = [] - self.transpile = transpile - - def append(self, child: "_TargetTreeNode"): - """ - Only appends children that are to be transpiled or that have children. - - :param child: The child node - """ - if child.transpile or child.children: - self.children.append(child) - - -class CodeWriter: - """ - A base class that provides basic code writing functionality. This class - implements a simple indentation/newline scheme that deriving classes may - use. - - :param level: The level to start indentation at - :param indent: The indent string to use - :param prefix: A prefix string to add to each line - :param kwargs: Any additional configuration parameters - """ - - rendered_: str - level_: int = 0 - prefix_: str = "" - indent_: str = " " * 4 - nl_: str = "\n" - - def __init__( - self, - level: int = level_, - indent: Optional[str] = indent_, - prefix: str = prefix_, - **kwargs, # pylint: disable=unused-argument - ) -> None: - self.rendered_ = "" - self.level_ = level - self.indent_ = indent or "" - self.prefix_ = prefix or "" - self.nl_ = self.nl_ if self.indent_ else "" # pylint: disable=C0103 - - def get_line(self, line: Optional[str]) -> str: - """ - Returns a line with indentation and newline. - """ - return f"{self.prefix_}{self.indent_ * self.level_}{line}{self.nl_}" - - def write_line(self, line: Optional[str]) -> None: - """ - Writes a line to the rendered code file, respecting - indentation/newline configuration for this generator. - - :param line: The code line to write - :return: - """ - if line is not None: - self.rendered_ += self.get_line(line) - - def indent(self, incr: int = 1) -> None: - """ - Step in one or more indentation levels. - - :param incr: The number of indentation levels to step into. Default: 1 - :return: - """ - self.level_ += incr - - def outdent(self, decr: int = 1) -> None: - """ - Step out one or more indentation levels. - - :param decr: The number of indentation levels to step out. Default: 1 - :return: - """ - self.level_ -= decr - self.level_ = max(0, self.level_) - - -class Transpiler(CodeWriter, metaclass=ABCMeta): - """ - An abstract base class for JavaScript generator types. This class defines a - basic generation API, and implements configurable indentation/newline - behavior. It also offers a toggle for ES5/ES6 mode that deriving classes - may use. - - To use this class derive from it and implement include_target() and - visit(). - - :param to_javascript: A callable that accepts a python artifact and returns - a transpiled object or primitive instantiation. - :param kwargs: A set of configuration parameters for the generator, see - above. - """ - - to_javascript_: Callable = to_js # pylint: disable=used-before-assignment - - parents_: List[Union[ModuleType, Type[Any]]] - target_: ResolvedTranspilerTarget - - overrides_: Dict[str, "OverrideNode"] - - @property - def target(self): - """The python artifact that is the target to transpile.""" - return self.target_ - - @property - def parents(self): - """ - When in visit() this returns the parents (modules and classes) of the - visited target. - """ - return [parent for parent in self.parents_ if parent is not self.target] - - @property - def context(self): - """ - The base template render context passed to overrides. Includes: - - - **transpiler**: The transpiler instance - """ - return {"transpiler": self} - - def __init__( - self, - to_javascript: Union[str, Callable] = to_javascript_, - overrides: Optional[Dict[str, "OverrideNode"]] = None, - **kwargs, - ) -> None: - super().__init__(**kwargs) - self.to_javascript = ( - to_javascript if callable(to_javascript) else import_string(to_javascript) - ) - self.overrides_ = overrides or {} - self.parents_ = [] - assert callable(self.to_javascript), "To_javascript is not callable!" - - def transpile_override( - self, - override: str, - default_impl: Union[str, Generator[Optional[str], None, None]], - context: Optional[Dict[str, Any]] = None, - ) -> Generator[str, None, None]: - """ - Returns a string of lines from a generator with the indentation and - newlines added. This is meant to be used in place during overrides, - so the first newline has no indents. So it will have the line prefix - of {{ default_impl }}. - - :param override: The name of the override to transpile - :param default_impl: The default implementation to use if the override - is not present. May be a single line string or a generator of - lines. - :param context: Any additional context to pass to the override render - """ - d_impl = default_impl - if isinstance(default_impl, Generator): - d_impl = "" - for idx, line in enumerate(default_impl): - if idx == 0: - d_impl += f"{line}{self.nl_}" - else: - d_impl += self.get_line(line) - d_impl.rstrip(self.nl_) - - yield from self.overrides_.pop(override).transpile( - {**self.context, "default_impl": SafeString(d_impl), **(context or {})} - ) - - @abstractmethod - def include_target(self, target: TranspilerTarget): - """ - Deriving transpilers must implement this method to filter targets - (modules or classes) in and out of transpilation. Transpilers are - expected to walk module trees and pick out supported python artifacts. - - :param target: The python artifact to filter in or out - :return: True if the target can be transpiled - """ - return True - - def transpile( # pylint: disable=too-many-branches, disable=too-many-statements - self, targets: TranspilerTargets - ) -> str: - """ - Generate and return javascript as a string given the targets. This - method iterates over the list of given targets, imports any strings - and builds a tree from targets the deriving transpiler filters - in via `include_target`. It then does a depth first traversal through - the tree to any leaf target nodes that were included and visits them - where any deriving class transpilation takes place. - - :param targets: The python targets to transpile - :return: The rendered JavaScript string - """ - root = _TargetTreeNode() - deduplicate_set: Set[Hashable] = set() - - def walk_class(cls: _TargetTreeNode): - for name, cls_member in vars(cls.target).items(): - if name.startswith("_"): - continue - if isinstance(cls_member, type) and cls_member not in deduplicate_set: - deduplicate_set.add(cls_member) - cls.append( - walk_class( - _TargetTreeNode(cls_member, self.include_target(cls_member)) - ) - ) - return cls - - for target in targets: - # do this instead of isinstance b/c types that inherit from strings - # may be targets - if isinstance(target, str): - if apps.is_installed(target): - target = { - app_config.name: app_config - for app_config in apps.get_app_configs() - }.get(target) - else: - try: - target = apps.get_app_config(target) - except LookupError: - parts = target.split(".") - tries = 0 - while True: - try: - tries += 1 - target = import_string( - ".".join( - parts[0 : None if tries == 1 else -(tries - 1)] - ) - ) - if tries > 1: - for attr in parts[-(tries - 1) :]: - target = getattr(target, attr) - break - except (ImportError, AttributeError): - if tries == 1: - try: - target = import_module(".".join(parts)) - except (ImportError, ModuleNotFoundError): - if len(parts) == 1: - raise - elif tries == len(parts): - raise - - node = _TargetTreeNode(target, self.include_target(target)) - - if node.target in deduplicate_set: - continue - - if isinstance(target, Hashable): - deduplicate_set.add(target) - - if isinstance(target, type): - root.append(walk_class(node)) - elif isinstance(target, ModuleType): - for _, member in vars(target).items(): - if isinstance(member, type): - node.append( - walk_class( - _TargetTreeNode(member, self.include_target(member)) - ) - ) - root.append(node) - elif isinstance(target, AppConfig): - root.append(node) - - if not root.transpile and not root.children: - raise ValueError(f"No targets were transpilable: {targets}") - - def visit_depth_first( - branch: _TargetTreeNode, is_last: bool = False, final: bool = True - ): - is_final = final and not branch.children - if branch.target: - for stm in self.enter_parent(branch.target, is_last, is_final): - self.write_line(stm) - - if branch.transpile: - self.target_ = branch.target - for stm in self.visit(branch.target, is_last, is_final): - self.write_line(stm) - - if branch.children: - for idx, child in enumerate(branch.children): - visit_depth_first( - child, - idx == len(branch.children) - 1, - (idx == len(branch.children) - 1) and final, - ) - - if branch.target: - for stm in self.exit_parent(branch.target, is_last, is_final): - self.write_line(stm) - - for line in self.start_visitation(): - self.write_line(line) - - visit_depth_first(root) - - for line in self.end_visitation(): - self.write_line(line) - - return self.rendered_ - - def enter_parent( - self, parent: ResolvedTranspilerTarget, is_last: bool, is_final: bool - ) -> Generator[Optional[str], None, None]: - """ - Enter and visit a target, pushing it onto the parent stack. - - :param parent: The target, class or module to transpile. - :param is_last: True if this is the last target that will be visited at - this level. - :param is_final: False if this is the last target that will be visited - at all. - :yield: javascript lines, writes nothing by default - """ - self.parents_.append(parent) - if isinstance(parent, ModuleType): - yield from self.enter_module(parent, is_last, is_final) - elif isinstance(parent, type): - yield from self.enter_class(parent, is_last, is_final) - - def exit_parent( - self, parent: ResolvedTranspilerTarget, is_last: bool, is_final: bool - ) -> Generator[Optional[str], None, None]: - """ - Exit a target, removing it from the parent stack. - - :param parent: The target, class or module that was just transpiled. - :param is_last: True if this is the last target that will be visited at - this level. - :param is_final: False if this is the last target that will be visited - at all. - :yield: javascript lines, writes nothing by default - """ - del self.parents_[-1] - if isinstance(parent, ModuleType): - yield from self.exit_module(parent, is_last, is_final) - elif isinstance(parent, type): - yield from self.exit_class(parent, is_last, is_final) - - def enter_module( - self, - module: ModuleType, # pylint: disable=unused-argument - is_last: bool, # pylint: disable=unused-argument - is_final: bool, # pylint: disable=unused-argument - ) -> Generator[Optional[str], None, None]: - """ - Transpile a module. - - :param module: The module to transpile - :param is_last: True if this is the last target at this level to - transpile. - :param is_final: True if this is the last target at all to transpile. - :yield: javascript lines, writes nothing by default - """ - yield None - - def exit_module( - self, - module: ModuleType, # pylint: disable=unused-argument - is_last: bool, # pylint: disable=unused-argument - is_final: bool, # pylint: disable=unused-argument - ) -> Generator[Optional[str], None, None]: - """ - Close transpilation of a module. - - :param module: The module that was just transpiled - :param is_last: True if this is the last target at this level to - transpile. - :param is_final: True if this is the last target at all to transpile. - :yield: javascript lines, writes nothing by default - """ - yield None - - def enter_class( - self, - cls: Type[Any], # pylint: disable=unused-argument - is_last: bool, # pylint: disable=unused-argument - is_final: bool, # pylint: disable=unused-argument - ) -> Generator[Optional[str], None, None]: - """ - Transpile a class. - - :param cls: The class to transpile - :param is_last: True if this is the last target at this level to - transpile. - :param is_final: True if this is the last target at all to transpile. - :yield: javascript lines, writes nothing by default - """ - yield None - - def exit_class( - self, - cls: Type[Any], # pylint: disable=unused-argument - is_last: bool, # pylint: disable=unused-argument - is_final: bool, # pylint: disable=unused-argument - ) -> Generator[Optional[str], None, None]: - """ - Close transpilation of a class. - - :param cls: The class that was just transpiled - :param is_last: True if this is the last target at this level to - transpile. - :param is_final: True if this is the last target at all to transpile. - :yield: javascript lines, writes nothing by default - """ - yield None - - def start_visitation(self) -> Generator[Optional[str], None, None]: - """ - Begin transpilation - called before visit(). Override this function - to do any initial code generation. - - :yield: javascript lines, writes nothing by default - """ - yield None - - def end_visitation(self) -> Generator[Optional[str], None, None]: - """ - End transpilation - called after all visit() calls have completed. - Override this function to do any wrap up code generation. - - :yield: javascript lines, writes nothing by default - """ - yield None - - @abstractmethod - def visit( - self, target: ResolvedTranspilerTarget, is_last: bool, is_final: bool - ) -> Generator[Optional[str], None, None]: - """ - Deriving transpilers must implement this method. - - :param target: The python target to transpile, will be either a class - a module or an installed Django app. - :param is_last: True if this is the last target that will be visited at - this level. - :param is_final: True if this is the last target that will be visited - at all. - :yield: lines of javascript - """ - - def to_js(self, value: Any): - """ - Return the javascript transpilation of the given value. - - :param value: The value to transpile - :return: A valid javascript code that represents the value - """ - return self.to_javascript(value) diff --git a/render_static/transpilers/base.py b/render_static/transpilers/base.py new file mode 100644 index 0000000..bea37ac --- /dev/null +++ b/render_static/transpilers/base.py @@ -0,0 +1,574 @@ +""" +Base transpiler components. +""" + +import json +import numbers +from abc import ABCMeta, abstractmethod +from collections.abc import Hashable +from datetime import date, datetime +from enum import Enum +from importlib import import_module +from types import ModuleType +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Collection, + Dict, + Generator, + List, + Optional, + Set, + Type, + Union, + cast, +) + +from django.apps import apps +from django.apps.config import AppConfig +from django.template.context import Context +from django.utils.module_loading import import_string +from django.utils.safestring import SafeString + +if TYPE_CHECKING: # pragma: no cover + from render_static.templatetags.render_static import OverrideNode + + +__all__ = [ + "CodeWriter", + "Transpiler", + "TranspilerTargets", + "TranspilerTarget", + "ResolvedTranspilerTarget", +] + +ResolvedTranspilerTarget = Union[Type, ModuleType, AppConfig] +TranspilerTarget = Union[ResolvedTranspilerTarget, str] +TranspilerTargets = Collection[TranspilerTarget] + + +def to_js(value: Any) -> str: + """ + Default javascript transpilation function for values. Simply adds quotes + if it's a string and falls back on json.dumps() for non-strings and non- + numerics. + + :param value: The value to transpile + :return: Valid javascript code that represents the value + """ + if isinstance(value, Enum): + value = value.value + if isinstance(value, numbers.Number): + return str(value) + if isinstance(value, str): + return f'"{value}"' + try: + return json.dumps(value) + except TypeError: + if isinstance(value, datetime): + return f'"{value.isoformat()}"' + return f'"{str(value)}"' + + +def to_js_datetime(value: Any) -> str: + """ + A javascript value transpilation function that transpiles python dates and + datetimes to javascript Date objects instead of strings. To use this + function in any of the transpilation routines pass it to the to_javascript + parameter on any of the template tags:: + + {% ... to_javascript="render_static.transpilers.to_js_datetime" %} + + :param value: The value to transpile + :return: Valid javascript code that represents the value + """ + if isinstance(value, date): + return f'new Date("{value.isoformat()}")' + return to_js(value) + + +class _TargetTreeNode: + """ + Simple tree node for tracking python target hierarchy. + + :param target: The target at this node + """ + + target: Optional[ResolvedTranspilerTarget] + children: List["_TargetTreeNode"] + transpile = False + + def __init__( + self, target: Optional[ResolvedTranspilerTarget] = None, transpile: bool = False + ): + self.target = target + self.children = [] + self.transpile = transpile + + def append(self, child: "_TargetTreeNode"): + """ + Only appends children that are to be transpiled or that have children. + + :param child: The child node + """ + if child.transpile or child.children: + self.children.append(child) + + +class CodeWriter: + """ + A base class that provides basic code writing functionality. This class + implements a simple indentation/newline scheme that deriving classes may + use. + + :param level: The level to start indentation at + :param indent: The indent string to use + :param prefix: A prefix string to add to each line + :param kwargs: Any additional configuration parameters + """ + + rendered_: str + level_: int = 0 + prefix_: str = "" + indent_: str = " " * 4 + nl_: str = "\n" + + def __init__( + self, + level: int = level_, + indent: Optional[str] = indent_, + prefix: str = prefix_, + **kwargs, + ) -> None: + self.rendered_ = "" + self.level_ = level + self.indent_ = indent or "" + self.prefix_ = prefix or "" + self.nl_ = self.nl_ if self.indent_ else "" + + def get_line(self, line: Optional[str]) -> str: + """ + Returns a line with indentation and newline. + """ + return f"{self.prefix_}{self.indent_ * self.level_}{line}{self.nl_}" + + def write_line(self, line: Optional[str]) -> None: + """ + Writes a line to the rendered code file, respecting + indentation/newline configuration for this generator. + + :param line: The code line to write + :return: + """ + if line is not None: + self.rendered_ += self.get_line(line) + + def indent(self, incr: int = 1) -> None: + """ + Step in one or more indentation levels. + + :param incr: The number of indentation levels to step into. Default: 1 + :return: + """ + self.level_ += incr + + def outdent(self, decr: int = 1) -> None: + """ + Step out one or more indentation levels. + + :param decr: The number of indentation levels to step out. Default: 1 + :return: + """ + self.level_ -= decr + self.level_ = max(0, self.level_) + + +class Transpiler(CodeWriter, metaclass=ABCMeta): + """ + An abstract base class for JavaScript generator types. This class defines a + basic generation API, and implements configurable indentation/newline + behavior. It also offers a toggle for ES5/ES6 mode that deriving classes + may use. + + To use this class derive from it and implement include_target() and + visit(). + + :param to_javascript: A callable that accepts a python artifact and returns + a transpiled object or primitive instantiation. + :param kwargs: A set of configuration parameters for the generator, see + above. + """ + + to_javascript_: Callable = to_js + + parents_: List[Union[ModuleType, Type]] + target_: ResolvedTranspilerTarget + + overrides_: Dict[str, "OverrideNode"] + + @property + def target(self): + """The python artifact that is the target to transpile.""" + return self.target_ + + @property + def parents(self): + """ + When in visit() this returns the parents (modules and classes) of the + visited target. + """ + return [parent for parent in self.parents_ if parent is not self.target] + + @property + def context(self): + """ + The base template render context passed to overrides. Includes: + + - **transpiler**: The transpiler instance + """ + return {"transpiler": self} + + def __init__( + self, + to_javascript: Union[str, Callable] = to_javascript_, + overrides: Optional[Dict[str, "OverrideNode"]] = None, + **kwargs, + ) -> None: + super().__init__(**kwargs) + self.to_javascript = ( + to_javascript if callable(to_javascript) else import_string(to_javascript) + ) + self.overrides_ = overrides or {} + self.parents_ = [] + assert callable(self.to_javascript), "To_javascript is not callable!" + + def transpile_override( + self, + override: str, + default_impl: Union[str, Generator[Optional[str], None, None]], + context: Optional[Dict[str, Any]] = None, + ) -> Generator[str, None, None]: + """ + Returns a string of lines from a generator with the indentation and + newlines added. This is meant to be used in place during overrides, + so the first newline has no indents. So it will have the line prefix + of {{ default_impl }}. + + :param override: The name of the override to transpile + :param default_impl: The default implementation to use if the override + is not present. May be a single line string or a generator of + lines. + :param context: Any additional context to pass to the override render + """ + d_impl = default_impl + if isinstance(default_impl, Generator): + d_impl = "" + for idx, line in enumerate(default_impl): + if idx == 0: + d_impl += f"{line}{self.nl_}" + else: + d_impl += self.get_line(line) + d_impl.rstrip(self.nl_) + + yield from self.overrides_.pop(override).transpile( + Context( + {**self.context, "default_impl": SafeString(d_impl), **(context or {})} + ) + ) + + @abstractmethod + def include_target(self, target: ResolvedTranspilerTarget) -> bool: + """ + Deriving transpilers must implement this method to filter targets + (modules or classes) in and out of transpilation. Transpilers are + expected to walk module trees and pick out supported python artifacts. + + :param target: The python artifact to filter in or out + :return: True if the target can be transpiled + """ + return True + + def transpile(self, targets: TranspilerTargets) -> str: + """ + Generate and return javascript as a string given the targets. This + method iterates over the list of given targets, imports any strings + and builds a tree from targets the deriving transpiler filters + in via `include_target`. It then does a depth first traversal through + the tree to any leaf target nodes that were included and visits them + where any deriving class transpilation takes place. + + :param targets: The python targets to transpile + :return: The rendered JavaScript string + """ + root = _TargetTreeNode() + deduplicate_set: Set[Hashable] = set() + + def walk_class(cls: _TargetTreeNode): + for name, cls_member in vars(cls.target).items(): + if name.startswith("_"): + continue + if isinstance(cls_member, type) and cls_member not in deduplicate_set: + deduplicate_set.add(cls_member) + cls.append( + walk_class( + _TargetTreeNode(cls_member, self.include_target(cls_member)) + ) + ) + return cls + + for target in targets: + # do this instead of isinstance b/c types that inherit from strings + # may be targets + if isinstance(target, str): + if apps.is_installed(target): + target = cast( + AppConfig, + { + app_config.name: app_config + for app_config in apps.get_app_configs() + }.get(target), + ) + else: + try: + target = apps.get_app_config(target) + except LookupError: + assert isinstance(target, str) + parts = target.split(".") + tries = 0 + while True: + try: + tries += 1 + target = import_string( + ".".join( + parts[0 : None if tries == 1 else -(tries - 1)] + ) + ) + if tries > 1: + for attr in parts[-(tries - 1) :]: + target = getattr(target, attr) + break + except (ImportError, AttributeError, ValueError) as err: + if tries == 1: + try: + target = import_module(".".join(parts)) + break + except (ImportError, ModuleNotFoundError): + if len(parts) == 1: + raise ImportError( + f"Unable to import {target}" + ) from err + elif tries == len(parts): + raise ImportError( + f"Unable to import {target}" + ) from err + + target = cast(ResolvedTranspilerTarget, target) + node = _TargetTreeNode(target, self.include_target(target)) + + if node.target in deduplicate_set: + continue + + if isinstance(target, Hashable): + deduplicate_set.add(target) + + if isinstance(target, type): + root.append(walk_class(node)) + elif isinstance(target, ModuleType): + for _, member in vars(target).items(): + if isinstance(member, type): + node.append( + walk_class( + _TargetTreeNode(member, self.include_target(member)) + ) + ) + root.append(node) + elif isinstance(target, AppConfig): + root.append(node) + + if not root.transpile and not root.children: + raise ValueError(f"No targets were transpilable: {targets}") + + def visit_depth_first( + branch: _TargetTreeNode, is_last: bool = False, final: bool = True + ): + is_final = final and not branch.children + if branch.target and not isinstance(branch.target, AppConfig): + for stm in self.enter_parent(branch.target, is_last, is_final): + self.write_line(stm) + + if branch.transpile and branch.target: + self.target_ = branch.target + for stm in self.visit(branch.target, is_last, is_final): + self.write_line(stm) + + if branch.children: + for idx, child in enumerate(branch.children): + visit_depth_first( + child, + idx == len(branch.children) - 1, + (idx == len(branch.children) - 1) and final, + ) + + if branch.target and not isinstance(branch.target, AppConfig): + for stm in self.exit_parent(branch.target, is_last, is_final): + self.write_line(stm) + + for line in self.start_visitation(): + self.write_line(line) + + visit_depth_first(root) + + for line in self.end_visitation(): + self.write_line(line) + + return self.rendered_ + + def enter_parent( + self, parent: Union[ModuleType, Type], is_last: bool, is_final: bool + ) -> Generator[Optional[str], None, None]: + """ + Enter and visit a target, pushing it onto the parent stack. + + :param parent: The target, class or module to transpile. + :param is_last: True if this is the last target that will be visited at + this level. + :param is_final: False if this is the last target that will be visited + at all. + :yield: javascript lines, writes nothing by default + """ + self.parents_.append(parent) + if isinstance(parent, ModuleType): + yield from self.enter_module(parent, is_last, is_final) + else: + yield from self.enter_class(parent, is_last, is_final) + + def exit_parent( + self, parent: Union[ModuleType, Type], is_last: bool, is_final: bool + ) -> Generator[Optional[str], None, None]: + """ + Exit a target, removing it from the parent stack. + + :param parent: The target, class or module that was just transpiled. + :param is_last: True if this is the last target that will be visited at + this level. + :param is_final: False if this is the last target that will be visited + at all. + :yield: javascript lines, writes nothing by default + """ + del self.parents_[-1] + if isinstance(parent, ModuleType): + yield from self.exit_module(parent, is_last, is_final) + else: + yield from self.exit_class(parent, is_last, is_final) + + def enter_module( + self, + module: ModuleType, + is_last: bool, + is_final: bool, + ) -> Generator[Optional[str], None, None]: + """ + Transpile a module. + + :param module: The module to transpile + :param is_last: True if this is the last target at this level to + transpile. + :param is_final: True if this is the last target at all to transpile. + :yield: javascript lines, writes nothing by default + """ + yield None + + def exit_module( + self, + module: ModuleType, + is_last: bool, + is_final: bool, + ) -> Generator[Optional[str], None, None]: + """ + Close transpilation of a module. + + :param module: The module that was just transpiled + :param is_last: True if this is the last target at this level to + transpile. + :param is_final: True if this is the last target at all to transpile. + :yield: javascript lines, writes nothing by default + """ + yield None + + def enter_class( + self, + cls: Type[Any], + is_last: bool, + is_final: bool, + ) -> Generator[Optional[str], None, None]: + """ + Transpile a class. + + :param cls: The class to transpile + :param is_last: True if this is the last target at this level to + transpile. + :param is_final: True if this is the last target at all to transpile. + :yield: javascript lines, writes nothing by default + """ + yield None + + def exit_class( + self, + cls: Type[Any], + is_last: bool, + is_final: bool, + ) -> Generator[Optional[str], None, None]: + """ + Close transpilation of a class. + + :param cls: The class that was just transpiled + :param is_last: True if this is the last target at this level to + transpile. + :param is_final: True if this is the last target at all to transpile. + :yield: javascript lines, writes nothing by default + """ + yield None + + def start_visitation(self) -> Generator[Optional[str], None, None]: + """ + Begin transpilation - called before visit(). Override this function + to do any initial code generation. + + :yield: javascript lines, writes nothing by default + """ + yield None + + def end_visitation(self) -> Generator[Optional[str], None, None]: + """ + End transpilation - called after all visit() calls have completed. + Override this function to do any wrap up code generation. + + :yield: javascript lines, writes nothing by default + """ + yield None + + @abstractmethod + def visit( + self, target: ResolvedTranspilerTarget, is_last: bool, is_final: bool + ) -> Generator[Optional[str], None, None]: + """ + Deriving transpilers must implement this method. + + :param target: The python target to transpile, will be either a class + a module or an installed Django app. + :param is_last: True if this is the last target that will be visited at + this level. + :param is_final: True if this is the last target that will be visited + at all. + :yield: lines of javascript + """ + + def to_js(self, value: Any): + """ + Return the javascript transpilation of the given value. + + :param value: The value to transpile + :return: A valid javascript code that represents the value + """ + return self.to_javascript(value) diff --git a/render_static/transpilers/defines_to_js.py b/render_static/transpilers/defines_to_js.py index a4c779f..26d1216 100644 --- a/render_static/transpilers/defines_to_js.py +++ b/render_static/transpilers/defines_to_js.py @@ -2,10 +2,14 @@ Built-in transpilers for python classes. Only one is provided that transpiles plain old data found on classes and their ancestors. """ + from types import ModuleType from typing import Any, Callable, Dict, Generator, Optional, Type, Union -from render_static.transpilers import ResolvedTranspilerTarget, Transpiler +from django.apps import AppConfig +from django.template.context import Context + +from render_static.transpilers.base import ResolvedTranspilerTarget, Transpiler class DefaultDefineTranspiler(Transpiler): @@ -74,9 +78,7 @@ class MyModel(models.Model): `Transpiler` params """ - include_member_: Callable[ - [Any], bool - ] = lambda name, member: name.isupper() # type: ignore + include_member_: Callable[[Any], bool] = lambda name, member: name.isupper() # type: ignore const_name_ = "defines" members_: Dict[str, Any] @@ -92,7 +94,7 @@ def members(self) -> Dict[str, Any]: return self.members_ @members.setter - def members(self, target: Union[ModuleType, Type[Any]]): + def members(self, target: Union[ModuleType, Type]): self.members_ = {} for ancestor in list(reversed(getattr(target, "__mro__", []))) + [target]: self.members_.update( @@ -134,7 +136,7 @@ def __init__( super().__init__(**kwargs) def visit( - self, target: Union[ModuleType, Type[Any]], is_last: bool, is_final: bool + self, target: ResolvedTranspilerTarget, is_last: bool, is_final: bool ) -> Generator[Optional[str], None, None]: """ Visit a target (module or class) and yield its defines as transpiled @@ -145,6 +147,9 @@ def visit( :param is_final: :return: """ + assert not isinstance( + target, AppConfig + ), "Unsupported transpiler target: AppConfig" self.members = target # type: ignore yield from self.visit_members(self.members, is_last=is_last, is_final=is_final) @@ -160,7 +165,7 @@ def end_visitation(self) -> Generator[Optional[str], None, None]: Lay down the closing brace for the const variable declaration. """ for _, override in self.overrides_.items(): - yield from override.transpile(self.context) + yield from override.transpile(Context(self.context)) self.outdent() yield "};" @@ -223,8 +228,8 @@ def visit_member( self, name: str, member: Any, - is_last: bool = False, # pylint: disable=unused-argument - is_final: bool = False, # pylint: disable=unused-argument + is_last: bool = False, + is_final: bool = False, ) -> Generator[Optional[str], None, None]: """ Visit a member of a class and yield its rendered javascript. diff --git a/render_static/transpilers/enums_to_js.py b/render_static/transpilers/enums_to_js.py index c025894..f581557 100644 --- a/render_static/transpilers/enums_to_js.py +++ b/render_static/transpilers/enums_to_js.py @@ -1,6 +1,7 @@ """ Transpiler tools for PEP 435 style python enumeration classes. """ + import sys import warnings from abc import abstractmethod @@ -8,11 +9,12 @@ from typing import Any, Collection, Dict, Generator, List, Optional, Set, Type, Union from django.db.models import IntegerChoices, TextChoices +from django.template.context import Context -from render_static.transpilers import Transpiler, TranspilerTarget +from render_static.transpilers.base import Transpiler, TranspilerTarget try: - from django.utils.decorators import classproperty # pylint: disable=C0412 + from django.utils.decorators import classproperty # type: ignore[attr-defined] except ImportError: from django.utils.functional import classproperty @@ -44,7 +46,7 @@ class EnumTranspiler(Transpiler): base class to write custom transpilers. """ - def include_target(self, target: TranspilerTarget): + def include_target(self, target: TranspilerTarget) -> bool: """ Deriving transpilers must implement this method to filter targets in and out of transpilation. Transpilers are expected to walk module trees @@ -54,7 +56,7 @@ def include_target(self, target: TranspilerTarget): :return: True if the target can be transpiled """ if isinstance(target, type) and issubclass(target, Enum): - return ( + return bool( target not in IGNORED_ENUMS and target.__module__ != "enum" and @@ -64,10 +66,9 @@ def include_target(self, target: TranspilerTarget): return False @abstractmethod - def visit( + def visit( # pyright: ignore[reportIncompatibleMethodOverride] self, enum: Type[Enum], # type: ignore - # pylint: disable=arguments-renamed is_last: bool, is_final: bool, ) -> Generator[Optional[str], None, None]: @@ -83,7 +84,7 @@ def visit( """ -class EnumClassWriter(EnumTranspiler): # pylint: disable=R0902 +class EnumClassWriter(EnumTranspiler): """ A PEP 435 transpiler that generates ES6 style classes in the style of https://github.com/rauschma/enumify @@ -419,7 +420,7 @@ def context(self): "to_string": self.to_string_, } - def __init__( # pylint: disable=R0913 + def __init__( self, class_name: str = class_name_pattern_, on_unrecognized: Union[str, UnrecognizedBehavior] = on_unrecognized_, @@ -477,9 +478,8 @@ def __init__( # pylint: disable=R0913 def visit( self, enum: Type[Enum], # type: ignore - # pylint: disable=arguments-renamed is_last: bool, - is_final: bool, # pylint: disable=unused-argument + is_final: bool, ) -> Generator[Optional[str], None, None]: """ Transpile the enum in sections. @@ -514,13 +514,11 @@ def visit( yield "" yield from self.iterator(enum) for _, override in self.overrides_.items(): - yield from override.transpile(self.context) + yield from override.transpile(Context(self.context)) self.outdent() yield "}" - def declaration( # pylint: disable=W0613 - self, enum: Type[Enum] - ) -> Generator[Optional[str], None, None]: + def declaration(self, enum: Type[Enum]) -> Generator[Optional[str], None, None]: """ Transpile the class declaration. @@ -556,9 +554,7 @@ def static_properties( for prop in self.class_properties: yield f"static {prop} = {self.to_js(getattr(enum, prop))};" - def constructor( # pylint: disable=W0613 - self, enum: Type[Enum] - ) -> Generator[Optional[str], None, None]: + def constructor(self, enum: Type[Enum]) -> Generator[Optional[str], None, None]: """ Transpile the constructor for the enum instances. @@ -603,9 +599,7 @@ def ci_compare(self) -> Generator[Optional[str], None, None]: self.outdent() yield "}" - def to_string( # pylint: disable=W0613 - self, enum: Type[Enum] - ) -> Generator[Optional[str], None, None]: + def to_string(self, enum: Type[Enum]) -> Generator[Optional[str], None, None]: """ Transpile the toString() method that converts enum instances to strings. diff --git a/render_static/transpilers/urls_to_js.py b/render_static/transpilers/urls_to_js.py index d70b4a1..2cea92d 100644 --- a/render_static/transpilers/urls_to_js.py +++ b/render_static/transpilers/urls_to_js.py @@ -14,16 +14,17 @@ from django import VERSION as DJANGO_VERSION from django.conf import settings from django.core.serializers.json import DjangoJSONEncoder +from django.template.context import Context from django.urls import URLPattern, URLResolver, reverse from django.urls.exceptions import NoReverseMatch -from django.urls.resolvers import RegexPattern, RoutePattern +from django.urls.resolvers import LocalePrefixPattern, RegexPattern, RoutePattern from render_static.exceptions import ReversalLimitHit, URLGenerationFailed from render_static.placeholders import ( resolve_placeholders, resolve_unnamed_placeholders, ) -from render_static.transpilers import ResolvedTranspilerTarget, Transpiler +from render_static.transpilers.base import ResolvedTranspilerTarget, Transpiler __all__ = [ "normalize_ns", @@ -105,8 +106,8 @@ def build_tree( ) -def _build_branch( # pylint: disable=R0913 - nodes: Iterable[URLPattern], +def _build_branch( + nodes: Iterable[Union[URLPattern, URLResolver]], included: bool, branch: Tuple[ Dict, Dict, Optional[str], Optional[Union[RegexPattern, RoutePattern]] @@ -190,7 +191,7 @@ def _build_branch( # pylint: disable=R0913 def _prune_tree( - tree: Tuple[Dict, Dict, Optional[str], Optional[Union[RegexPattern, RoutePattern]]] + tree: Tuple[Dict, Dict, Optional[str], Optional[Union[RegexPattern, RoutePattern]]], ) -> Tuple[ Tuple[Dict, Dict, Optional[str], Optional[Union[RegexPattern, RoutePattern]]], int ]: @@ -257,7 +258,7 @@ class BaseURLTranspiler(Transpiler): attributes that contain a list of URLPattern and URLResolver objects. """ - def include_target(self, target: ResolvedTranspilerTarget): + def include_target(self, target: ResolvedTranspilerTarget) -> bool: """ Only transpile artifacts that have url pattern/resolver lists in them. @@ -319,7 +320,7 @@ class URLTreeVisitor(BaseURLTranspiler): exclude_: Optional[Iterable[str]] = None @property - def context(self): + def context(self) -> Dict[str, Any]: """ The template render context passed to overrides. In addition to :attr:`render_static.transpilers.Transpiler.context`. @@ -366,7 +367,7 @@ def exit_namespace(self, namespace) -> Generator[Optional[str], None, None]: visitation exit """ - def visit_pattern( # pylint: disable=R0914, R0915, R0912 + def visit_pattern( self, endpoint: URLPattern, qname: str, @@ -398,18 +399,17 @@ def visit_pattern( # pylint: disable=R0914, R0915, R0912 # first, pull out any named or unnamed parameters that comprise this # pattern - def get_params(pattern: Union[RoutePattern, RegexPattern]) -> Dict[str, Any]: - if isinstance(pattern, RoutePattern): + def get_params( + pattern: Union[RoutePattern, RegexPattern, LocalePrefixPattern], + ) -> Dict[str, Any]: + if isinstance(pattern, (RoutePattern, LocalePrefixPattern)): return { var: {"converter": converter.__class__, "app_name": app_name} for var, converter in pattern.converters.items() } - if isinstance(pattern, RegexPattern): - return { - var: {"app_name": app_name} - for var in pattern.regex.groupindex.keys() - } - raise URLGenerationFailed(f"Unrecognized pattern type: {type(pattern)}") + return { + var: {"app_name": app_name} for var in pattern.regex.groupindex.keys() + } params = get_params(endpoint.pattern) @@ -417,7 +417,7 @@ def get_params(pattern: Union[RoutePattern, RegexPattern]) -> Dict[str, Any]: params = {**params, **get_params(rt_pattern)} # does this url have unnamed or named params? - unnamed = False + unnamed = 0 if not params and endpoint.pattern.regex.groups > 0: unnamed = endpoint.pattern.regex.groups @@ -431,11 +431,11 @@ def get_params(pattern: Union[RoutePattern, RegexPattern]) -> Dict[str, Any]: ) # if we have parameters, resolve the placeholders for them - if params or unnamed or endpoint.default_args: # pylint: disable=R1702 + if params or unnamed or endpoint.default_args: if unnamed: resolved_placeholders = itertools.product( *resolve_unnamed_placeholders( - url_name=endpoint.name, nargs=unnamed, app_name=app_name + url_name=endpoint.name or "", nargs=unnamed, app_name=app_name ), ) non_capturing = str(endpoint.pattern.regex).count("(?:") @@ -446,7 +446,7 @@ def get_params(pattern: Union[RoutePattern, RegexPattern]) -> Dict[str, Any]: resolved_placeholders = itertools.chain( # type: ignore resolved_placeholders, *resolve_unnamed_placeholders( - url_name=endpoint.name, + url_name=endpoint.name or "", nargs=unnamed - non_capturing, app_name=app_name, ), @@ -506,9 +506,7 @@ def get_params(pattern: Union[RoutePattern, RegexPattern]) -> Dict[str, Any]: idx: var for var, idx in composite_regex.groupindex.items() } - for idx, value in enumerate( # pylint: disable=W0612 - mtch.groups(), start=1 - ): + for idx, value in enumerate(mtch.groups(), start=1): if unnamed: replacements.append((mtch.span(idx), Substitute(idx - 1))) else: @@ -520,7 +518,7 @@ def get_params(pattern: Union[RoutePattern, RegexPattern]) -> Dict[str, Any]: ) url_idx = 0 - path = [] + path: List[Union[str, Substitute]] = [] for rpl in replacements: while url_idx <= rpl[0][0]: path.append(placeholder_url[url_idx]) @@ -797,7 +795,7 @@ class SimpleURLWriter(URLTreeVisitor): raise_on_not_found_ = True @property - def context(self): + def context(self) -> Dict[str, Any]: """ The template render context passed to overrides. In addition to :attr:`render_static.transpilers.urls_to_js.URLTreeVisitor.context`. @@ -832,7 +830,7 @@ def close_visit(self) -> Generator[Optional[str], None, None]: :yield: nothing """ for _, override in self.overrides_.items(): - yield from override.transpile(self.context) + yield from override.transpile(Context(self.context)) def enter_namespace(self, namespace: str) -> Generator[Optional[str], None, None]: """ @@ -940,7 +938,7 @@ class ClassURLWriter(URLTreeVisitor): .. code-block:: js+django {% urls_to_js - visitor="render_static.ClassURLWriter" + visitor="render_static.transpilers.ClassURLWriter" class_name='URLResolver' indent=' ' %} @@ -1018,9 +1016,7 @@ def class_jdoc(self) -> Generator[Optional[str], None, None]: * query parameters in the reversed url. * * @class - */""".split( - "\n" - ): + */""".split("\n"): yield comment_line[8:] def constructor_jdoc(self) -> Generator[Optional[str], None, None]: @@ -1035,9 +1031,7 @@ def constructor_jdoc(self) -> Generator[Optional[str], None, None]: * @param {Object} options - The options object. * @param {string} options.namespace - When provided, namespace will * prefix all reversed paths with the given namespace. - */""".split( - "\n" - ): + */""".split("\n"): yield comment_line[8:] def match_jdoc(self) -> Generator[Optional[str], None, None]: @@ -1057,9 +1051,7 @@ def match_jdoc(self) -> Generator[Optional[str], None, None]: * @param {string[]} expected - An array of expected arguments. * @param {Object.} defaults - An object mapping * default arguments to their values. - */""".split( - "\n" - ): + */""".split("\n"): yield comment_line[8:] def reverse_jdoc(self) -> Generator[Optional[str], None, None]: @@ -1082,9 +1074,7 @@ def reverse_jdoc(self) -> Generator[Optional[str], None, None]: * positional arguments. * @param {Object.} options.query - URL query * parameters to add to the end of the reversed url. - */""".split( - "\n" - ): + */""".split("\n"): yield comment_line[8:] def constructor(self) -> Generator[Optional[str], None, None]: @@ -1166,9 +1156,7 @@ def impl() -> Generator[str, None, None]: * * @param {Object} object1 - The first object to compare. * @param {Object} object2 - The second object to compare. - */""".split( - "\n" - ): + */""".split("\n"): yield comment_line[12:] yield "deepEqual(object1, object2) {" self.indent() @@ -1190,9 +1178,7 @@ def is_object(self) -> Generator[Optional[str], None, None]: * Given a variable, return true if it is an object. * * @param {Object} object - The variable to check. - */""".split( - "\n" - ): + */""".split("\n"): yield comment_line[12:] yield "isObject(object) {" self.indent() @@ -1335,7 +1321,7 @@ def impl() -> Generator[str, None, None]: self.outdent() yield "}" - def init_visit( # pylint: disable=R0915 + def init_visit( self, ) -> Generator[Optional[str], None, None]: """ @@ -1369,7 +1355,7 @@ def close_visit(self) -> Generator[Optional[str], None, None]: """ yield "}" for _, override in self.overrides_.items(): - yield from override.transpile(self.context) + yield from override.transpile(Context(self.context)) self.outdent() yield "};" diff --git a/render_static/tests/app1/__init__.py b/tests/__init__.py old mode 100755 new mode 100644 similarity index 100% rename from render_static/tests/app1/__init__.py rename to tests/__init__.py diff --git a/render_static/tests/app1/static_jinja2/batch_test/{{ site_name }}/__init__.py b/tests/app1/__init__.py old mode 100644 new mode 100755 similarity index 100% rename from render_static/tests/app1/static_jinja2/batch_test/{{ site_name }}/__init__.py rename to tests/app1/__init__.py diff --git a/render_static/tests/app1/apps.py b/tests/app1/apps.py similarity index 95% rename from render_static/tests/app1/apps.py rename to tests/app1/apps.py index 2cd1d70..7c1105f 100755 --- a/render_static/tests/app1/apps.py +++ b/tests/app1/apps.py @@ -35,5 +35,5 @@ def to_url(self, value): class App1Config(AppConfig): - name = "render_static.tests.app1" + name = "tests.app1" label = name.replace(".", "_") diff --git a/render_static/tests/app1/defines.py b/tests/app1/defines.py similarity index 100% rename from render_static/tests/app1/defines.py rename to tests/app1/defines.py diff --git a/render_static/tests/app1/static_jinja2/app1/html/app_jinja2.html b/tests/app1/static_jinja2/app1/html/app_jinja2.html similarity index 100% rename from render_static/tests/app1/static_jinja2/app1/html/app_jinja2.html rename to tests/app1/static_jinja2/app1/html/app_jinja2.html diff --git a/render_static/tests/app1/static_jinja2/batch_test/__init__.py b/tests/app1/static_jinja2/batch_test/__init__.py similarity index 100% rename from render_static/tests/app1/static_jinja2/batch_test/__init__.py rename to tests/app1/static_jinja2/batch_test/__init__.py diff --git a/render_static/tests/app1/static_templates/batch_test/{{ site_name }}/__init__.py b/tests/app1/static_jinja2/batch_test/{{ site_name }}/__init__.py similarity index 100% rename from render_static/tests/app1/static_templates/batch_test/{{ site_name }}/__init__.py rename to tests/app1/static_jinja2/batch_test/{{ site_name }}/__init__.py diff --git a/render_static/tests/app1/static_jinja2/batch_test/{{ site_name }}/file1.py b/tests/app1/static_jinja2/batch_test/{{ site_name }}/file1.py similarity index 100% rename from render_static/tests/app1/static_jinja2/batch_test/{{ site_name }}/file1.py rename to tests/app1/static_jinja2/batch_test/{{ site_name }}/file1.py diff --git a/render_static/tests/app1/static_jinja2/batch_test/{{ site_name }}/file2.html b/tests/app1/static_jinja2/batch_test/{{ site_name }}/file2.html similarity index 100% rename from render_static/tests/app1/static_jinja2/batch_test/{{ site_name }}/file2.html rename to tests/app1/static_jinja2/batch_test/{{ site_name }}/file2.html diff --git a/render_static/tests/app1/static_jinja2/batch_test/{{ site_name }}/{{ sub_dir }}/.gitkeep b/tests/app1/static_jinja2/batch_test/{{ site_name }}/{{ sub_dir }}/.gitkeep similarity index 100% rename from render_static/tests/app1/static_jinja2/batch_test/{{ site_name }}/{{ sub_dir }}/.gitkeep rename to tests/app1/static_jinja2/batch_test/{{ site_name }}/{{ sub_dir }}/.gitkeep diff --git a/render_static/tests/app1/static_templates/app1/html/base.html b/tests/app1/static_templates/app1/html/base.html similarity index 100% rename from render_static/tests/app1/static_templates/app1/html/base.html rename to tests/app1/static_templates/app1/html/base.html diff --git a/render_static/tests/app1/static_templates/app1/html/hello.html b/tests/app1/static_templates/app1/html/hello.html similarity index 100% rename from render_static/tests/app1/static_templates/app1/html/hello.html rename to tests/app1/static_templates/app1/html/hello.html diff --git a/render_static/tests/app1/static_templates/app1/html/nominal1.html b/tests/app1/static_templates/app1/html/nominal1.html similarity index 100% rename from render_static/tests/app1/static_templates/app1/html/nominal1.html rename to tests/app1/static_templates/app1/html/nominal1.html diff --git a/render_static/tests/app1/static_templates/batch_test/__init__.py b/tests/app1/static_templates/batch_test/__init__.py similarity index 100% rename from render_static/tests/app1/static_templates/batch_test/__init__.py rename to tests/app1/static_templates/batch_test/__init__.py diff --git a/render_static/tests/app2/__init__.py b/tests/app1/static_templates/batch_test/{{ site_name }}/__init__.py old mode 100755 new mode 100644 similarity index 100% rename from render_static/tests/app2/__init__.py rename to tests/app1/static_templates/batch_test/{{ site_name }}/__init__.py diff --git a/render_static/tests/app1/static_templates/batch_test/{{ site_name }}/file1.py b/tests/app1/static_templates/batch_test/{{ site_name }}/file1.py similarity index 100% rename from render_static/tests/app1/static_templates/batch_test/{{ site_name }}/file1.py rename to tests/app1/static_templates/batch_test/{{ site_name }}/file1.py diff --git a/render_static/tests/app1/static_templates/batch_test/{{ site_name }}/file2.html b/tests/app1/static_templates/batch_test/{{ site_name }}/file2.html similarity index 100% rename from render_static/tests/app1/static_templates/batch_test/{{ site_name }}/file2.html rename to tests/app1/static_templates/batch_test/{{ site_name }}/file2.html diff --git a/render_static/tests/app1/static_templates/batch_test/{{ site_name }}/{{ sub_dir }}/.gitkeep b/tests/app1/static_templates/batch_test/{{ site_name }}/{{ sub_dir }}/.gitkeep similarity index 100% rename from render_static/tests/app1/static_templates/batch_test/{{ site_name }}/{{ sub_dir }}/.gitkeep rename to tests/app1/static_templates/batch_test/{{ site_name }}/{{ sub_dir }}/.gitkeep diff --git a/render_static/tests/app1/static_templates/exclusive/template1.html b/tests/app1/static_templates/exclusive/template1.html similarity index 100% rename from render_static/tests/app1/static_templates/exclusive/template1.html rename to tests/app1/static_templates/exclusive/template1.html diff --git a/render_static/tests/app1/static_templates/exclusive/template6.html b/tests/app1/static_templates/exclusive/template6.html similarity index 100% rename from render_static/tests/app1/static_templates/exclusive/template6.html rename to tests/app1/static_templates/exclusive/template6.html diff --git a/render_static/tests/app1/urls.py b/tests/app1/urls.py similarity index 100% rename from render_static/tests/app1/urls.py rename to tests/app1/urls.py diff --git a/render_static/tests/app3/__init__.py b/tests/app2/__init__.py similarity index 100% rename from render_static/tests/app3/__init__.py rename to tests/app2/__init__.py diff --git a/render_static/tests/app2/apps.py b/tests/app2/apps.py similarity index 72% rename from render_static/tests/app2/apps.py rename to tests/app2/apps.py index 29fd381..13f1d94 100755 --- a/render_static/tests/app2/apps.py +++ b/tests/app2/apps.py @@ -2,5 +2,5 @@ class App2Config(AppConfig): - name = "render_static.tests.app2" + name = "tests.app2" label = name.replace(".", "_") diff --git a/render_static/tests/app2/custom_jinja2/app1/html/app_jinja2.html b/tests/app2/custom_jinja2/app1/html/app_jinja2.html similarity index 100% rename from render_static/tests/app2/custom_jinja2/app1/html/app_jinja2.html rename to tests/app2/custom_jinja2/app1/html/app_jinja2.html diff --git a/render_static/tests/app2/custom_templates/nominal_fs.html b/tests/app2/custom_templates/nominal_fs.html similarity index 100% rename from render_static/tests/app2/custom_templates/nominal_fs.html rename to tests/app2/custom_templates/nominal_fs.html diff --git a/render_static/tests/app2/custom_templates/nominal_fs2.html b/tests/app2/custom_templates/nominal_fs2.html similarity index 100% rename from render_static/tests/app2/custom_templates/nominal_fs2.html rename to tests/app2/custom_templates/nominal_fs2.html diff --git a/render_static/tests/app2/static_jinja2/app1/glob1.js b/tests/app2/static_jinja2/app1/glob1.js similarity index 100% rename from render_static/tests/app2/static_jinja2/app1/glob1.js rename to tests/app2/static_jinja2/app1/glob1.js diff --git a/render_static/tests/app2/static_jinja2/app1/glob2.js b/tests/app2/static_jinja2/app1/glob2.js similarity index 100% rename from render_static/tests/app2/static_jinja2/app1/glob2.js rename to tests/app2/static_jinja2/app1/glob2.js diff --git a/render_static/tests/app2/static_jinja2/app1/html/app_jinja2.html b/tests/app2/static_jinja2/app1/html/app_jinja2.html similarity index 100% rename from render_static/tests/app2/static_jinja2/app1/html/app_jinja2.html rename to tests/app2/static_jinja2/app1/html/app_jinja2.html diff --git a/render_static/tests/app2/static_jinja2/app1/other.js b/tests/app2/static_jinja2/app1/other.js similarity index 100% rename from render_static/tests/app2/static_jinja2/app1/other.js rename to tests/app2/static_jinja2/app1/other.js diff --git a/render_static/tests/app2/static_jinja2/app2/html/inheritance.html b/tests/app2/static_jinja2/app2/html/inheritance.html similarity index 100% rename from render_static/tests/app2/static_jinja2/app2/html/inheritance.html rename to tests/app2/static_jinja2/app2/html/inheritance.html diff --git a/render_static/tests/app2/static_templates/app1/html/inheritance.html b/tests/app2/static_templates/app1/html/inheritance.html similarity index 100% rename from render_static/tests/app2/static_templates/app1/html/inheritance.html rename to tests/app2/static_templates/app1/html/inheritance.html diff --git a/render_static/tests/app2/static_templates/app1/html/nominal1.html b/tests/app2/static_templates/app1/html/nominal1.html similarity index 100% rename from render_static/tests/app2/static_templates/app1/html/nominal1.html rename to tests/app2/static_templates/app1/html/nominal1.html diff --git a/render_static/tests/app2/static_templates/app1/html/nominal2.html b/tests/app2/static_templates/app1/html/nominal2.html similarity index 100% rename from render_static/tests/app2/static_templates/app1/html/nominal2.html rename to tests/app2/static_templates/app1/html/nominal2.html diff --git a/render_static/tests/app2/static_templates/exclusive/template1.html b/tests/app2/static_templates/exclusive/template1.html similarity index 100% rename from render_static/tests/app2/static_templates/exclusive/template1.html rename to tests/app2/static_templates/exclusive/template1.html diff --git a/render_static/tests/app2/static_templates/exclusive/template2.html b/tests/app2/static_templates/exclusive/template2.html similarity index 100% rename from render_static/tests/app2/static_templates/exclusive/template2.html rename to tests/app2/static_templates/exclusive/template2.html diff --git a/render_static/tests/app2/static_templates/exclusive/template5.html b/tests/app2/static_templates/exclusive/template5.html similarity index 100% rename from render_static/tests/app2/static_templates/exclusive/template5.html rename to tests/app2/static_templates/exclusive/template5.html diff --git a/render_static/tests/app2/urls.py b/tests/app2/urls.py similarity index 84% rename from render_static/tests/app2/urls.py rename to tests/app2/urls.py index 330723c..2aeb2dc 100644 --- a/render_static/tests/app2/urls.py +++ b/tests/app2/urls.py @@ -4,7 +4,7 @@ app_name = "app2" urlpatterns = [ - path("app1_inc/", include("render_static.tests.app1.urls")), + path("app1_inc/", include("tests.app1.urls")), path("app2/", TestView.as_view(), name="app2_pth"), path("app2/", TestView.as_view(), name="app2_pth"), path( diff --git a/render_static/tests/chain/__init__.py b/tests/app3/__init__.py similarity index 100% rename from render_static/tests/chain/__init__.py rename to tests/app3/__init__.py diff --git a/render_static/tests/app3/apps.py b/tests/app3/apps.py similarity index 72% rename from render_static/tests/app3/apps.py rename to tests/app3/apps.py index 3bb0e6c..b221149 100755 --- a/render_static/tests/app3/apps.py +++ b/tests/app3/apps.py @@ -2,5 +2,5 @@ class App3Config(AppConfig): - name = "render_static.tests.app3" + name = "tests.app3" label = name.replace(".", "_") diff --git a/render_static/tests/app3/urls.py b/tests/app3/urls.py similarity index 100% rename from render_static/tests/app3/urls.py rename to tests/app3/urls.py diff --git a/render_static/tests/bad_pattern.py b/tests/bad_pattern.py similarity index 84% rename from render_static/tests/bad_pattern.py rename to tests/bad_pattern.py index ab469e4..aaf0f52 100644 --- a/render_static/tests/bad_pattern.py +++ b/tests/bad_pattern.py @@ -2,7 +2,7 @@ from django.urls import path -from render_static.tests.views import TestView +from tests.views import TestView class Unrecognized: diff --git a/render_static/tests/enum_app/__init__.py b/tests/chain/__init__.py old mode 100644 new mode 100755 similarity index 100% rename from render_static/tests/enum_app/__init__.py rename to tests/chain/__init__.py diff --git a/render_static/tests/chain/apps.py b/tests/chain/apps.py similarity index 72% rename from render_static/tests/chain/apps.py rename to tests/chain/apps.py index edd19d8..de70381 100755 --- a/render_static/tests/chain/apps.py +++ b/tests/chain/apps.py @@ -2,5 +2,5 @@ class ChainConfig(AppConfig): - name = "render_static.tests.chain" + name = "tests.chain" label = name.replace(".", "_") diff --git a/tests/chain/urls.py b/tests/chain/urls.py new file mode 100644 index 0000000..fcd0211 --- /dev/null +++ b/tests/chain/urls.py @@ -0,0 +1,11 @@ +from django.urls import include, path, re_path + +app_name = "chain" + +urlpatterns = [ + path("chain//", include("tests.spa.urls", namespace="spa")), + re_path( + r"^chain/(?P\w+)/", + include("tests.spa.urls", namespace="spa_re"), + ), +] diff --git a/tests/context.py b/tests/context.py new file mode 100644 index 0000000..4077020 --- /dev/null +++ b/tests/context.py @@ -0,0 +1,7 @@ +""" +A context as a python module. +""" + +VARIABLE1 = "value1" + +other_variable = "value2" diff --git a/render_static/tests/defines.py b/tests/defines.py similarity index 100% rename from render_static/tests/defines.py rename to tests/defines.py diff --git a/render_static/tests/defines2.py b/tests/defines2.py similarity index 100% rename from render_static/tests/defines2.py rename to tests/defines2.py diff --git a/render_static/tests/empty_defines.py b/tests/empty_defines.py similarity index 100% rename from render_static/tests/empty_defines.py rename to tests/empty_defines.py diff --git a/render_static/tests/enum_app/migrations/__init__.py b/tests/enum_app/__init__.py similarity index 100% rename from render_static/tests/enum_app/migrations/__init__.py rename to tests/enum_app/__init__.py diff --git a/render_static/tests/enum_app/apps.py b/tests/enum_app/apps.py similarity index 71% rename from render_static/tests/enum_app/apps.py rename to tests/enum_app/apps.py index 86a0b05..572dab9 100644 --- a/render_static/tests/enum_app/apps.py +++ b/tests/enum_app/apps.py @@ -2,5 +2,5 @@ class EnumAppConfig(AppConfig): - name = "render_static.tests.enum_app" + name = "tests.enum_app" label = name.replace(".", "_") diff --git a/render_static/tests/enum_app/defines.py b/tests/enum_app/defines.py similarity index 100% rename from render_static/tests/enum_app/defines.py rename to tests/enum_app/defines.py diff --git a/render_static/tests/enum_app/enums.py b/tests/enum_app/enums.py similarity index 100% rename from render_static/tests/enum_app/enums.py rename to tests/enum_app/enums.py diff --git a/render_static/tests/enum_app/migrations/0001_initial.py b/tests/enum_app/migrations/0001_initial.py similarity index 100% rename from render_static/tests/enum_app/migrations/0001_initial.py rename to tests/enum_app/migrations/0001_initial.py diff --git a/render_static/tests/enum_app/templatetags/__init__.py b/tests/enum_app/migrations/__init__.py similarity index 100% rename from render_static/tests/enum_app/templatetags/__init__.py rename to tests/enum_app/migrations/__init__.py diff --git a/render_static/tests/enum_app/models.py b/tests/enum_app/models.py similarity index 100% rename from render_static/tests/enum_app/models.py rename to tests/enum_app/models.py diff --git a/render_static/tests/enum_app/static_templates/enum_app/enum.js b/tests/enum_app/static_templates/enum_app/enum.js similarity index 100% rename from render_static/tests/enum_app/static_templates/enum_app/enum.js rename to tests/enum_app/static_templates/enum_app/enum.js diff --git a/render_static/tests/enum_app/static_templates/enum_app/test.js b/tests/enum_app/static_templates/enum_app/test.js similarity index 100% rename from render_static/tests/enum_app/static_templates/enum_app/test.js rename to tests/enum_app/static_templates/enum_app/test.js diff --git a/render_static/tests/examples/__init__.py b/tests/enum_app/templatetags/__init__.py similarity index 100% rename from render_static/tests/examples/__init__.py rename to tests/enum_app/templatetags/__init__.py diff --git a/render_static/tests/enum_app/templatetags/enum_test.py b/tests/enum_app/templatetags/enum_test.py similarity index 100% rename from render_static/tests/enum_app/templatetags/enum_test.py rename to tests/enum_app/templatetags/enum_test.py diff --git a/render_static/tests/enum_app/urls.py b/tests/enum_app/urls.py similarity index 100% rename from render_static/tests/enum_app/urls.py rename to tests/enum_app/urls.py diff --git a/render_static/tests/ex_urls.py b/tests/ex_urls.py similarity index 100% rename from render_static/tests/ex_urls.py rename to tests/ex_urls.py diff --git a/render_static/tests/examples/migrations/__init__.py b/tests/examples/__init__.py similarity index 100% rename from render_static/tests/examples/migrations/__init__.py rename to tests/examples/__init__.py diff --git a/render_static/tests/examples/apps.py b/tests/examples/apps.py similarity index 71% rename from render_static/tests/examples/apps.py rename to tests/examples/apps.py index e5b5f5d..35934ae 100644 --- a/render_static/tests/examples/apps.py +++ b/tests/examples/apps.py @@ -2,5 +2,5 @@ class ExamplesConfig(AppConfig): - name = "render_static.tests.examples" + name = "tests.examples" label = name.replace(".", "_") diff --git a/render_static/tests/examples/migrations/0001_initial.py b/tests/examples/migrations/0001_initial.py similarity index 100% rename from render_static/tests/examples/migrations/0001_initial.py rename to tests/examples/migrations/0001_initial.py diff --git a/render_static/tests/resources/__init__.py b/tests/examples/migrations/__init__.py similarity index 100% rename from render_static/tests/resources/__init__.py rename to tests/examples/migrations/__init__.py diff --git a/render_static/tests/examples/models.py b/tests/examples/models.py similarity index 100% rename from render_static/tests/examples/models.py rename to tests/examples/models.py diff --git a/tests/examples/static/examples/defines.js b/tests/examples/static/examples/defines.js new file mode 100644 index 0000000..d7f4e73 --- /dev/null +++ b/tests/examples/static/examples/defines.js @@ -0,0 +1,25 @@ +const defines = { + ExampleModel: { + DEFINE1: "D1", + DEFINE2: "D2", + DEFINE3: "D3", + DEFINES: [["D1", "Define 1"], ["D2", "Define 2"], ["D3", "Define 3"]], + Color: { + RED: "R", + GREEN: "G", + BLUE: "B", + }, + MapBoxStyle: { + STREETS: 1, + OUTDOORS: 2, + LIGHT: 3, + DARK: 4, + SATELLITE: 5, + SATELLITE_STREETS: 6, + NAVIGATION_DAY: 7, + NAVIGATION_NIGHT: 8, + }, + }, +}; + +console.log(JSON.stringify(defines)); diff --git a/tests/examples/static/examples/enums.js b/tests/examples/static/examples/enums.js new file mode 100644 index 0000000..3896c7c --- /dev/null +++ b/tests/examples/static/examples/enums.js @@ -0,0 +1,84 @@ +class Color { + + static RED = new Color("R", "RED", "Red", [1, 0, 0], "ff0000"); + static GREEN = new Color("G", "GREEN", "Green", [0, 1, 0], "00ff00"); + static BLUE = new Color("B", "BLUE", "Blue", [0, 0, 1], "0000ff"); + + constructor (value, name, label, rgb, hex) { + this.value = value; + this.name = name; + this.label = label; + this.rgb = rgb; + this.hex = hex; + } + + toString() { + return this.value; + } + + static get(value) { + if (value instanceof this) { + return value; + } + + for (const en of this) { + if (en.value === value) { + return en; + } + } + throw new TypeError(`No Color enumeration maps to value ${value}`); + } + + static [Symbol.iterator]() { + return [Color.RED, Color.GREEN, Color.BLUE][Symbol.iterator](); + } +} +class MapBoxStyle { + + static STREETS = new MapBoxStyle(1, "STREETS", "Streets", "streets", 11, "mapbox://styles/mapbox/streets-v11"); + static OUTDOORS = new MapBoxStyle(2, "OUTDOORS", "Outdoors", "outdoors", 11, "mapbox://styles/mapbox/outdoors-v11"); + static LIGHT = new MapBoxStyle(3, "LIGHT", "Light", "light", 10, "mapbox://styles/mapbox/light-v10"); + static DARK = new MapBoxStyle(4, "DARK", "Dark", "dark", 10, "mapbox://styles/mapbox/dark-v10"); + static SATELLITE = new MapBoxStyle(5, "SATELLITE", "Satellite", "satellite", 9, "mapbox://styles/mapbox/satellite-v9"); + static SATELLITE_STREETS = new MapBoxStyle(6, "SATELLITE_STREETS", "Satellite Streets", "satellite-streets", 11, "mapbox://styles/mapbox/satellite-streets-v11"); + static NAVIGATION_DAY = new MapBoxStyle(7, "NAVIGATION_DAY", "Navigation Day", "navigation-day", 1, "mapbox://styles/mapbox/navigation-day-v1"); + static NAVIGATION_NIGHT = new MapBoxStyle(8, "NAVIGATION_NIGHT", "Navigation Night", "navigation-night", 1, "mapbox://styles/mapbox/navigation-night-v1"); + + static docs = "https://mapbox.com"; + + constructor (value, name, label, slug, version, uri) { + this.value = value; + this.name = name; + this.label = label; + this.slug = slug; + this.version = version; + this.uri = uri; + } + + toString() { + return this.uri; + } + + static get(value) { + if (value instanceof this) { + return value; + } + + for (const en of this) { + if (en.value === value) { + return en; + } + } + throw new TypeError(`No MapBoxStyle enumeration maps to value ${value}`); + } + + static [Symbol.iterator]() { + return [MapBoxStyle.STREETS, MapBoxStyle.OUTDOORS, MapBoxStyle.LIGHT, MapBoxStyle.DARK, MapBoxStyle.SATELLITE, MapBoxStyle.SATELLITE_STREETS, MapBoxStyle.NAVIGATION_DAY, MapBoxStyle.NAVIGATION_NIGHT][Symbol.iterator](); + } +} + + +console.log(Color.BLUE === Color.get('B')); +for (const color of Color) { + console.log(color); +} diff --git a/tests/examples/static/examples/readme_url_usage.js b/tests/examples/static/examples/readme_url_usage.js new file mode 100644 index 0000000..8f0907a --- /dev/null +++ b/tests/examples/static/examples/readme_url_usage.js @@ -0,0 +1,144 @@ + +/** + * A url resolver class that provides an interface very similar to + * Django's reverse() function. This interface is nearly identical to + * reverse() with a few caveats: + * + * - Python type coercion is not available, so care should be taken to + * pass in argument inputs that are in the expect string format. + * - Not all reversal behavior can be replicated but these are corner + * cases that are not likely to be correct url specification to + * begin with. + * - The reverse function also supports a query option to include url + * query parameters in the reversed url. + * + * @class + */ + class URLResolver { + + + /** + * Instantiate this url resolver. + * + * @param {Object} options - The options object. + * @param {string} options.namespace - When provided, namespace will + * prefix all reversed paths with the given namespace. + */ + constructor(options=null) { + this.options = options || {}; + if (this.options.hasOwnProperty("namespace")) { + this.namespace = this.options.namespace; + if (!this.namespace.endsWith(":")) { + this.namespace += ":"; + } + } else { + this.namespace = ""; + } + } + + + /** + * Given a set of args and kwargs and an expected set of arguments and + * a default mapping, return True if the inputs work for the given set. + * + * @param {Object} kwargs - The object holding the reversal named + * arguments. + * @param {string[]} args - The array holding the positional reversal + * arguments. + * @param {string[]} expected - An array of expected arguments. + * @param {Object.} defaults - An object mapping + * default arguments to their values. + */ + #match(kwargs, args, expected, defaults={}) { + if (defaults) { + kwargs = Object.assign({}, kwargs); + for (const [key, val] of Object.entries(defaults)) { + if (kwargs.hasOwnProperty(key)) { + if (kwargs[key] !== val && JSON.stringify(kwargs[key]) !== JSON.stringify(val) && !expected.includes(key)) { return false; } + if (!expected.includes(key)) { delete kwargs[key]; } + } + } + } + if (Array.isArray(expected)) { + return Object.keys(kwargs).length === expected.length && expected.every(value => kwargs.hasOwnProperty(value)); + } else if (expected) { + return args.length === expected; + } else { + return Object.keys(kwargs).length === 0 && args.length === 0; + } + } + + + /** + * Reverse a Django url. This method is nearly identical to Django's + * reverse function, with an additional option for URL parameters. See + * the class docstring for caveats. + * + * @param {string} qname - The name of the url to reverse. Namespaces + * are supported using `:` as a delimiter as with Django's reverse. + * @param {Object} options - The options object. + * @param {string} options.kwargs - The object holding the reversal + * named arguments. + * @param {string[]} options.args - The array holding the reversal + * positional arguments. + * @param {Object.} options.query - URL query + * parameters to add to the end of the reversed url. + */ + reverse(qname, options={}) { + if (this.namespace) { + qname = `${this.namespace}${qname.replace(this.namespace, "")}`; + } + const kwargs = options.kwargs || {}; + const args = options.args || []; + const query = options.query || {}; + let url = this.urls; + for (const ns of qname.split(':')) { + if (ns && url) { url = url.hasOwnProperty(ns) ? url[ns] : null; } + } + if (url) { + let pth = url(kwargs, args); + if (typeof pth === "string") { + if (Object.keys(query).length !== 0) { + const params = new URLSearchParams(); + for (const [key, value] of Object.entries(query)) { + if (value === null || value === '') continue; + if (Array.isArray(value)) value.forEach(element => params.append(key, element)); + else params.append(key, value); + } + const qryStr = params.toString(); + if (qryStr) return `${pth.replace(/\/+$/, '')}?${qryStr}`; + } + return pth; + } + } + throw new TypeError(`No reversal available for parameters at path: ${qname}`); + } + + urls = { + "different": (kwargs={}, args=[]) => { + if (this.#match(kwargs, args, ["arg1","arg2"])) { return `/different/${kwargs["arg1"]}/${kwargs["arg2"]}`; } + }, + "simple": (kwargs={}, args=[]) => { + if (this.#match(kwargs, args, ["arg1"])) { return `/simple/${kwargs["arg1"]}`; } + if (this.#match(kwargs, args)) { return "/simple"; } + }, + } +}; + + +// /different/143/emma +const urls = new URLResolver(); +console.log(urls.reverse('different', {kwargs: {'arg1': 143, 'arg2': 'emma'}})); + +// reverse also supports query parameters +// /different/143/emma?intarg=0&listarg=A&listarg=B&listarg=C +console.log(urls.reverse( + 'different', + { + kwargs: {arg1: 143, arg2: 'emma'}, + query: { + intarg: 0, + listarg: ['A', 'B', 'C'] + } + } +)); diff --git a/tests/examples/static_templates/examples/defines.js b/tests/examples/static_templates/examples/defines.js new file mode 100644 index 0000000..ddb959f --- /dev/null +++ b/tests/examples/static_templates/examples/defines.js @@ -0,0 +1,2 @@ +{% defines_to_js defines="tests.examples.models" %} +console.log(JSON.stringify(defines)); diff --git a/render_static/tests/examples/static_templates/examples/enums.js b/tests/examples/static_templates/examples/enums.js similarity index 64% rename from render_static/tests/examples/static_templates/examples/enums.js rename to tests/examples/static_templates/examples/enums.js index ee8475b..8c3811c 100644 --- a/render_static/tests/examples/static_templates/examples/enums.js +++ b/tests/examples/static_templates/examples/enums.js @@ -1,4 +1,4 @@ -{% enums_to_js "render_static.tests.examples.models" %} +{% enums_to_js "tests.examples.models" %} console.log(Color.BLUE === Color.get('B')); for (const color of Color) { diff --git a/render_static/tests/examples/static_templates/examples/readme_url_usage.js b/tests/examples/static_templates/examples/readme_url_usage.js similarity index 100% rename from render_static/tests/examples/static_templates/examples/readme_url_usage.js rename to tests/examples/static_templates/examples/readme_url_usage.js diff --git a/render_static/tests/examples/urls.py b/tests/examples/urls.py similarity index 100% rename from render_static/tests/examples/urls.py rename to tests/examples/urls.py diff --git a/render_static/tests/expected/HELLO_U.html b/tests/expected/HELLO_U.html similarity index 100% rename from render_static/tests/expected/HELLO_U.html rename to tests/expected/HELLO_U.html diff --git a/render_static/tests/expected/app1_jinja2.html b/tests/expected/app1_jinja2.html similarity index 100% rename from render_static/tests/expected/app1_jinja2.html rename to tests/expected/app1_jinja2.html diff --git a/render_static/tests/expected/app1_template1.html b/tests/expected/app1_template1.html similarity index 100% rename from render_static/tests/expected/app1_template1.html rename to tests/expected/app1_template1.html diff --git a/render_static/tests/expected/app1_template6.html b/tests/expected/app1_template6.html similarity index 100% rename from render_static/tests/expected/app1_template6.html rename to tests/expected/app1_template6.html diff --git a/render_static/tests/expected/app2_jinja2.html b/tests/expected/app2_jinja2.html similarity index 100% rename from render_static/tests/expected/app2_jinja2.html rename to tests/expected/app2_jinja2.html diff --git a/render_static/tests/expected/app2_template1.html b/tests/expected/app2_template1.html similarity index 100% rename from render_static/tests/expected/app2_template1.html rename to tests/expected/app2_template1.html diff --git a/render_static/tests/expected/app2_template2.html b/tests/expected/app2_template2.html similarity index 100% rename from render_static/tests/expected/app2_template2.html rename to tests/expected/app2_template2.html diff --git a/render_static/tests/expected/app2_template5.html b/tests/expected/app2_template5.html similarity index 100% rename from render_static/tests/expected/app2_template5.html rename to tests/expected/app2_template5.html diff --git a/render_static/tests/expected/ctx_override.html b/tests/expected/ctx_override.html similarity index 100% rename from render_static/tests/expected/ctx_override.html rename to tests/expected/ctx_override.html diff --git a/render_static/tests/expected/ctx_override2.html b/tests/expected/ctx_override2.html similarity index 100% rename from render_static/tests/expected/ctx_override2.html rename to tests/expected/ctx_override2.html diff --git a/render_static/tests/expected/ctx_override_cmdline.html b/tests/expected/ctx_override_cmdline.html similarity index 100% rename from render_static/tests/expected/ctx_override_cmdline.html rename to tests/expected/ctx_override_cmdline.html diff --git a/render_static/tests/expected/dest_override.html b/tests/expected/dest_override.html similarity index 100% rename from render_static/tests/expected/dest_override.html rename to tests/expected/dest_override.html diff --git a/render_static/tests/expected/glb2_template1.html b/tests/expected/glb2_template1.html similarity index 100% rename from render_static/tests/expected/glb2_template1.html rename to tests/expected/glb2_template1.html diff --git a/render_static/tests/expected/glb2_template4.html b/tests/expected/glb2_template4.html similarity index 100% rename from render_static/tests/expected/glb2_template4.html rename to tests/expected/glb2_template4.html diff --git a/render_static/tests/expected/glb_template1.html b/tests/expected/glb_template1.html similarity index 100% rename from render_static/tests/expected/glb_template1.html rename to tests/expected/glb_template1.html diff --git a/render_static/tests/expected/glb_template2.html b/tests/expected/glb_template2.html similarity index 100% rename from render_static/tests/expected/glb_template2.html rename to tests/expected/glb_template2.html diff --git a/render_static/tests/expected/glb_template3.html b/tests/expected/glb_template3.html similarity index 100% rename from render_static/tests/expected/glb_template3.html rename to tests/expected/glb_template3.html diff --git a/render_static/tests/expected/hello_l.html b/tests/expected/hello_l.html similarity index 100% rename from render_static/tests/expected/hello_l.html rename to tests/expected/hello_l.html diff --git a/render_static/tests/expected/inheritance.html b/tests/expected/inheritance.html similarity index 100% rename from render_static/tests/expected/inheritance.html rename to tests/expected/inheritance.html diff --git a/render_static/tests/expected/inheritance_jinja2.html b/tests/expected/inheritance_jinja2.html similarity index 100% rename from render_static/tests/expected/inheritance_jinja2.html rename to tests/expected/inheritance_jinja2.html diff --git a/render_static/tests/expected/multi_1_jinja2.html b/tests/expected/multi_1_jinja2.html similarity index 100% rename from render_static/tests/expected/multi_1_jinja2.html rename to tests/expected/multi_1_jinja2.html diff --git a/render_static/tests/expected/multi_2_jinja2.html b/tests/expected/multi_2_jinja2.html similarity index 100% rename from render_static/tests/expected/multi_2_jinja2.html rename to tests/expected/multi_2_jinja2.html diff --git a/render_static/tests/expected/nominal1.html b/tests/expected/nominal1.html similarity index 100% rename from render_static/tests/expected/nominal1.html rename to tests/expected/nominal1.html diff --git a/render_static/tests/expected/nominal2.html b/tests/expected/nominal2.html similarity index 100% rename from render_static/tests/expected/nominal2.html rename to tests/expected/nominal2.html diff --git a/render_static/tests/expected/nominal_fs.html b/tests/expected/nominal_fs.html similarity index 100% rename from render_static/tests/expected/nominal_fs.html rename to tests/expected/nominal_fs.html diff --git a/render_static/tests/expected/nominal_fs2.html b/tests/expected/nominal_fs2.html similarity index 100% rename from render_static/tests/expected/nominal_fs2.html rename to tests/expected/nominal_fs2.html diff --git a/render_static/tests/expected/nominal_jinja2.html b/tests/expected/nominal_jinja2.html similarity index 100% rename from render_static/tests/expected/nominal_jinja2.html rename to tests/expected/nominal_jinja2.html diff --git a/render_static/tests/expected/wildcard_test.js b/tests/expected/wildcard_test.js similarity index 100% rename from render_static/tests/expected/wildcard_test.js rename to tests/expected/wildcard_test.js diff --git a/render_static/tests/js_reverse_urls.py b/tests/js_reverse_urls.py similarity index 100% rename from render_static/tests/js_reverse_urls.py rename to tests/js_reverse_urls.py diff --git a/render_static/tests/spa/__init__.py b/tests/resources/__init__.py old mode 100755 new mode 100644 similarity index 100% rename from render_static/tests/spa/__init__.py rename to tests/resources/__init__.py diff --git a/render_static/tests/resources/bad.json b/tests/resources/bad.json similarity index 100% rename from render_static/tests/resources/bad.json rename to tests/resources/bad.json diff --git a/render_static/tests/resources/bad.yaml b/tests/resources/bad.yaml similarity index 100% rename from render_static/tests/resources/bad.yaml rename to tests/resources/bad.yaml diff --git a/render_static/tests/resources/bad_code.py b/tests/resources/bad_code.py similarity index 100% rename from render_static/tests/resources/bad_code.py rename to tests/resources/bad_code.py diff --git a/render_static/tests/resources/context.json b/tests/resources/context.json similarity index 100% rename from render_static/tests/resources/context.json rename to tests/resources/context.json diff --git a/render_static/tests/resources/context.py b/tests/resources/context.py similarity index 100% rename from render_static/tests/resources/context.py rename to tests/resources/context.py diff --git a/render_static/tests/resources/context.yaml b/tests/resources/context.yaml similarity index 100% rename from render_static/tests/resources/context.yaml rename to tests/resources/context.yaml diff --git a/render_static/tests/resources/context_embedded.py b/tests/resources/context_embedded.py similarity index 100% rename from render_static/tests/resources/context_embedded.py rename to tests/resources/context_embedded.py diff --git a/render_static/tests/resources/override.yaml b/tests/resources/override.yaml similarity index 100% rename from render_static/tests/resources/override.yaml rename to tests/resources/override.yaml diff --git a/render_static/tests/settings.py b/tests/settings.py similarity index 88% rename from render_static/tests/settings.py rename to tests/settings.py index b63d001..5dd1e63 100644 --- a/render_static/tests/settings.py +++ b/tests/settings.py @@ -15,7 +15,7 @@ } } -ROOT_URLCONF = "render_static.tests.urls" +ROOT_URLCONF = "tests.urls" TEMPLATES = [ { @@ -43,11 +43,11 @@ ) INSTALLED_APPS = ( - "render_static.tests.examples", - "render_static.tests.enum_app", - "render_static.tests.app1", - "render_static.tests.app2", - "render_static.tests.app3", + "tests.examples", + "tests.enum_app", + "tests.app1", + "tests.app2", + "tests.app3", "render_static", "django_typer", "django.contrib.auth", diff --git a/render_static/tests/traverse/__init__.py b/tests/spa/__init__.py old mode 100644 new mode 100755 similarity index 100% rename from render_static/tests/traverse/__init__.py rename to tests/spa/__init__.py diff --git a/render_static/tests/spa/apps.py b/tests/spa/apps.py similarity index 72% rename from render_static/tests/spa/apps.py rename to tests/spa/apps.py index 02afeb1..a488970 100755 --- a/render_static/tests/spa/apps.py +++ b/tests/spa/apps.py @@ -2,5 +2,5 @@ class SPAConfig(AppConfig): - name = "render_static.tests.spa" + name = "tests.spa" label = name.replace(".", "_") diff --git a/tests/spa/static_templates/spa/urls.js b/tests/spa/static_templates/spa/urls.js new file mode 100644 index 0000000..07b1c2c --- /dev/null +++ b/tests/spa/static_templates/spa/urls.js @@ -0,0 +1 @@ +{% urls_to_js visitor="render_static.transpilers.ClassURLWriter" es5=es5|default:False include=include %} diff --git a/render_static/tests/spa/templates/spa/index.html b/tests/spa/templates/spa/index.html similarity index 100% rename from render_static/tests/spa/templates/spa/index.html rename to tests/spa/templates/spa/index.html diff --git a/render_static/tests/spa/urls.py b/tests/spa/urls.py similarity index 100% rename from render_static/tests/spa/urls.py rename to tests/spa/urls.py diff --git a/render_static/tests/spa/views.py b/tests/spa/views.py similarity index 100% rename from render_static/tests/spa/views.py rename to tests/spa/views.py diff --git a/tests/spa_urls.py b/tests/spa_urls.py new file mode 100644 index 0000000..810be5f --- /dev/null +++ b/tests/spa_urls.py @@ -0,0 +1,6 @@ +from django.urls import include, path + +urlpatterns = [ + path("spa1/", include("tests.spa.urls", namespace="spa1")), + path("spa2/", include("tests.spa.urls", namespace="spa2")), +] diff --git a/render_static/tests/static_templates/batch_fs_test0.html b/tests/static_templates/batch_fs_test0.html similarity index 100% rename from render_static/tests/static_templates/batch_fs_test0.html rename to tests/static_templates/batch_fs_test0.html diff --git a/render_static/tests/static_templates/batch_fs_test1.html b/tests/static_templates/batch_fs_test1.html similarity index 100% rename from render_static/tests/static_templates/batch_fs_test1.html rename to tests/static_templates/batch_fs_test1.html diff --git a/render_static/tests/static_templates/exclusive/template1.html b/tests/static_templates/exclusive/template1.html similarity index 100% rename from render_static/tests/static_templates/exclusive/template1.html rename to tests/static_templates/exclusive/template1.html diff --git a/render_static/tests/static_templates/exclusive/template2.html b/tests/static_templates/exclusive/template2.html similarity index 100% rename from render_static/tests/static_templates/exclusive/template2.html rename to tests/static_templates/exclusive/template2.html diff --git a/render_static/tests/static_templates/exclusive/template3.html b/tests/static_templates/exclusive/template3.html similarity index 100% rename from render_static/tests/static_templates/exclusive/template3.html rename to tests/static_templates/exclusive/template3.html diff --git a/render_static/tests/static_templates/multi_test.jinja2 b/tests/static_templates/multi_test.jinja2 similarity index 100% rename from render_static/tests/static_templates/multi_test.jinja2 rename to tests/static_templates/multi_test.jinja2 diff --git a/render_static/tests/static_templates/nominal.jinja2 b/tests/static_templates/nominal.jinja2 similarity index 100% rename from render_static/tests/static_templates/nominal.jinja2 rename to tests/static_templates/nominal.jinja2 diff --git a/render_static/tests/static_templates/nominal_fs.html b/tests/static_templates/nominal_fs.html similarity index 100% rename from render_static/tests/static_templates/nominal_fs.html rename to tests/static_templates/nominal_fs.html diff --git a/render_static/tests/static_templates/subdir/batch_fs_test2.html b/tests/static_templates/subdir/batch_fs_test2.html similarity index 100% rename from render_static/tests/static_templates/subdir/batch_fs_test2.html rename to tests/static_templates/subdir/batch_fs_test2.html diff --git a/render_static/tests/static_templates2/exclusive/template1.html b/tests/static_templates2/exclusive/template1.html similarity index 100% rename from render_static/tests/static_templates2/exclusive/template1.html rename to tests/static_templates2/exclusive/template1.html diff --git a/render_static/tests/static_templates2/exclusive/template4.html b/tests/static_templates2/exclusive/template4.html similarity index 100% rename from render_static/tests/static_templates2/exclusive/template4.html rename to tests/static_templates2/exclusive/template4.html diff --git a/render_static/tests/tests.py b/tests/test_core.py similarity index 95% rename from render_static/tests/tests.py rename to tests/test_core.py index e1ddb5d..6ab7a31 100755 --- a/render_static/tests/tests.py +++ b/tests/test_core.py @@ -15,7 +15,8 @@ from django.template.exceptions import TemplateDoesNotExist from django.test import TestCase, override_settings -from render_static import resolve_context, resource +from render_static.context import resolve_context +from render_static.resource import resource from render_static.engine import StaticTemplateEngine from render_static.exceptions import InvalidContext from render_static.origin import AppOrigin, Origin @@ -71,8 +72,8 @@ def empty_or_dne(directory): class AppOriginTestCase(TestCase): def test_equality(self): - test_app1 = apps.get_app_config("render_static_tests_app1") - test_app2 = apps.get_app_config("render_static_tests_app2") + test_app1 = apps.get_app_config("tests_app1") + test_app2 = apps.get_app_config("tests_app2") origin1 = AppOrigin( name="/path/to/tmpl.html", template_name="to/tmpl.html", app=test_app1 @@ -411,7 +412,7 @@ def test_no_selector_templates_found(self): @override_settings( STATIC_TEMPLATES={ - "context": "tests.generate_context1", + "context": "tests.test_core.generate_context1", "templates": { "app1/html/hello.html": { "context": generate_context2, @@ -1324,14 +1325,32 @@ def test_python_context(self): {"context": "python"}, ) + def test_module_context(self): + from tests import context + + self.assertEqual( + resolve_context(context), + { + "VARIABLE1": "value1", + "other_variable": "value2", + }, + ) + + def test_module_import_context(self): + self.assertEqual( + resolve_context("tests.context"), + { + "VARIABLE1": "value1", + "other_variable": "value2", + }, + ) + @pytest.mark.skipif( not importlib_resources, reason="importlib_resources not available" ) def test_pickle_context_resource(self): self.assertEqual( - resolve_context( - resource("render_static.tests.resources", "context.pickle") - ), + resolve_context(resource("tests.resources", "context.pickle")), {"context": "pickle"}, ) @@ -1340,10 +1359,10 @@ def test_pickle_context_resource(self): ) def test_json_context_resource(self): self.assertEqual( - resolve_context(resource("render_static.tests.resources", "context.json")), + resolve_context(resource("tests.resources", "context.json")), {"context": "json"}, ) - from render_static.tests import resources + from tests import resources self.assertEqual( resolve_context(resource(resources, "context.json")), {"context": "json"} @@ -1354,19 +1373,17 @@ def test_json_context_resource(self): ) def test_python_context_resource(self): self.assertEqual( - resolve_context(resource("render_static.tests.resources", "context.py")), + resolve_context(resource("tests.resources", "context.py")), {"context": "python"}, ) def test_python_context_embedded_import(self): self.assertEqual( - resolve_context("render_static.tests.resources.context_embedded.context"), + resolve_context("tests.resources.context_embedded.context"), {"context": "embedded"}, ) self.assertEqual( - resolve_context( - "render_static.tests.resources.context_embedded.get_context" - ), + resolve_context("tests.resources.context_embedded.get_context"), {"context": "embedded_callable"}, ) @@ -1376,27 +1393,19 @@ def test_python_context_embedded_import(self): def test_bad_contexts(self): self.assertRaises( InvalidContext, - lambda: resolve_context( - "render_static.tests.resources.context_embedded.not_a_dict" - ), + lambda: resolve_context("tests.resources.context_embedded.not_a_dict"), ) self.assertRaises( InvalidContext, - lambda: resolve_context( - resource("render_static.tests.resources", "bad.pickle") - ), + lambda: resolve_context(resource("tests.resources", "bad.pickle")), ) self.assertRaises( InvalidContext, - lambda: resolve_context( - resource("render_static.tests.resources", "not_a_dict.pickle") - ), + lambda: resolve_context(resource("tests.resources", "not_a_dict.pickle")), ) self.assertRaises( InvalidContext, - lambda: resolve_context( - resource("render_static.tests.resources", "bad_code.py") - ), + lambda: resolve_context(resource("tests.resources", "bad_code.py")), ) self.assertRaises( InvalidContext, @@ -1412,12 +1421,12 @@ def test_bad_contexts(self): ) self.assertRaises( InvalidContext, - lambda: resolve_context(resource("render_static.tests.resources", "dne")), + lambda: resolve_context(resource("tests.resources", "dne")), ) self.assertRaises( InvalidContext, - lambda: resolve_context(resource("render_static.tests.dne", "dne")), + lambda: resolve_context(resource("tests.dne", "dne")), ) def tearDown(self): @@ -1738,44 +1747,21 @@ def test_loc_mem_completion(self): self.assertTrue("base.html" in completions) self.assertTrue("examples/enums.js" in completions) + stdout.truncate(0) + stdout.seek(0) -@pytest.mark.skipif(bool(jinja2), reason="jinja2 installed") -class TestJinja2MissingImportLoaders(TestCase): - def test_jinja2_loader_imports(self): - from render_static.loaders.jinja2 import ( - StaticChoiceLoader, - StaticDictLoader, - StaticFileSystemBatchLoader, - StaticFileSystemLoader, - StaticFunctionLoader, - StaticModuleLoader, - StaticPackageLoader, - StaticPrefixLoader, - ) - - with self.assertRaises(ImportError): - StaticFileSystemLoader() - - with self.assertRaises(ImportError): - StaticFileSystemBatchLoader() - - with self.assertRaises(ImportError): - StaticPackageLoader() - - with self.assertRaises(ImportError): - StaticPrefixLoader() - - with self.assertRaises(ImportError): - StaticFunctionLoader() - - with self.assertRaises(ImportError): - StaticDictLoader() - - with self.assertRaises(ImportError): - StaticChoiceLoader() - - with self.assertRaises(ImportError): - StaticModuleLoader() + with contextlib.redirect_stdout(stdout): + call_command( + "shellcompletion", "complete", "renderstatic app1/h", stdout=stdout + ) + completions = stdout.getvalue() + self.assertTrue("app1/html/base.html" in completions) + self.assertTrue("app1/html/hello.html" in completions) + self.assertTrue("app1/html/nominal2.html" in completions) + self.assertFalse("app1/urls.js" in completions) + self.assertFalse("app1/enums.js" in completions) + self.assertFalse("app1/examples/readme_url_usage.js" in completions) + self.assertFalse("examples/enums.js" in completions) def test_batch_loader_mixin_not_impl(): diff --git a/render_static/tests/examples_tests.py b/tests/test_examples.py similarity index 97% rename from render_static/tests/examples_tests.py rename to tests/test_examples.py index 451e2a0..68acb29 100644 --- a/render_static/tests/examples_tests.py +++ b/tests/test_examples.py @@ -1,6 +1,7 @@ """ All examples from the documentation should be tested here! """ + import re import shutil import subprocess @@ -12,14 +13,14 @@ from django.test import override_settings from django.urls import reverse -from render_static.tests.js_tests import ( +from tests.test_js import ( GLOBAL_STATIC_DIR, EnumComparator, StructureDiff, URLJavascriptMixin, run_js_file, ) -from render_static.tests.tests import BaseTestCase +from tests.test_core import BaseTestCase from render_static.transpilers.urls_to_js import ClassURLWriter try: @@ -129,7 +130,7 @@ def test_readme_enums(self): "render_static.loaders.StaticLocMemLoader", { "color.js": """ -{% enums_to_js enums="render_static.tests.examples.models.ExampleModel.Color" %} +{% enums_to_js enums="tests.examples.models.ExampleModel.Color" %} {# to override a function we must pass its name as the argument #} {% override 'get' %} static get(value) { @@ -186,7 +187,7 @@ def test_override_example(self): @override_settings( - ROOT_URLCONF="render_static.tests.examples.urls", + ROOT_URLCONF="tests.examples.urls", STATIC_TEMPLATES={ "ENGINES": [ { diff --git a/render_static/tests/_jinja2_tests.py b/tests/test_jinja2.py similarity index 89% rename from render_static/tests/_jinja2_tests.py rename to tests/test_jinja2.py index ea02f50..a263f28 100644 --- a/render_static/tests/_jinja2_tests.py +++ b/tests/test_jinja2.py @@ -1,6 +1,7 @@ import contextlib import filecmp import os +import pytest from io import StringIO from django.core.exceptions import ImproperlyConfigured @@ -10,16 +11,21 @@ from django.test import TestCase, override_settings from django.urls import reverse -from render_static.backends import StaticDjangoTemplates, StaticJinja2Templates -from render_static.engine import StaticTemplateEngine -from render_static.loaders.jinja2 import StaticDictLoader, StaticFileSystemLoader -from render_static.tests import defines -from render_static.tests.js_tests import ( +try: + from render_static.backends import StaticDjangoTemplates + from render_static.backends.jinja2 import StaticJinja2Templates + from render_static.engine import StaticTemplateEngine + from render_static.loaders.jinja2 import StaticDictLoader, StaticFileSystemLoader +except ImportError: + pytest.skip(allow_module_level=True, reason="Jinja2 is not installed") + +from tests import defines +from tests.test_js import ( ClassURLWriter, DefinesToJavascriptTest, URLJavascriptMixin, ) -from render_static.tests.tests import ( +from tests.test_core import ( APP1_STATIC_DIR, APP2_STATIC_DIR, BATCH_RENDER_TEMPLATES, @@ -67,7 +73,7 @@ def test_django_templates(self): STATIC_TEMPLATES={ "ENGINES": [ { - "BACKEND": "render_static.backends.StaticJinja2Templates", + "BACKEND": "render_static.backends.jinja2.StaticJinja2Templates", "APP_DIRS": True, } ], @@ -93,7 +99,7 @@ def test_jinja2_templates(self): STATIC_TEMPLATES={ "ENGINES": [ { - "BACKEND": "render_static.backends.StaticJinja2Templates", + "BACKEND": "render_static.backends.jinja2.StaticJinja2Templates", "DIRS": [STATIC_TEMP_DIR, STATIC_TEMP_DIR2], "APP_DIRS": True, "OPTIONS": {"app_dir": "static_templates"}, @@ -125,7 +131,7 @@ def validate_first_loader(self): STATIC_TEMPLATES={ "ENGINES": [ { - "BACKEND": "render_static.backends.StaticJinja2Templates", + "BACKEND": "render_static.backends.jinja2.StaticJinja2Templates", "DIRS": [STATIC_TEMP_DIR], "APP_DIRS": True, } @@ -196,7 +202,7 @@ def test_tab_completion(self): STATIC_TEMPLATES={ "ENGINES": [ { - "BACKEND": "render_static.backends.StaticJinja2Templates", + "BACKEND": "render_static.backends.jinja2.StaticJinja2Templates", "OPTIONS": {"loader": StaticFileSystemLoader(STATIC_TEMP_DIR)}, } ], @@ -225,7 +231,7 @@ def test_fs_loader(self): STATIC_TEMPLATES={ "ENGINES": [ { - "BACKEND": "render_static.backends.StaticJinja2Templates", + "BACKEND": "render_static.backends.jinja2.StaticJinja2Templates", "DIRS": [STATIC_TEMP_DIR], "APP_DIRS": True, "OPTIONS": {"app_dir": "custom_jinja2"}, @@ -277,7 +283,7 @@ def test_engines(self): }, { "NAME": "IDENTICAL", - "BACKEND": "render_static.backends.StaticJinja2Templates", + "BACKEND": "render_static.backends.jinja2.StaticJinja2Templates", "APP_DIRS": True, }, ] @@ -295,7 +301,7 @@ def test_engines(self): }, { "NAME": "DIFFERENT", - "BACKEND": "render_static.backends.StaticJinja2Templates", + "BACKEND": "render_static.backends.jinja2.StaticJinja2Templates", "APP_DIRS": True, }, ] @@ -310,7 +316,7 @@ def test_suspicious_selector_jinja2_appdirs(self): { "ENGINES": [ { - "BACKEND": "render_static.backends.StaticJinja2Templates", + "BACKEND": "render_static.backends.jinja2.StaticJinja2Templates", "APP_DIRS": True, } ] @@ -329,7 +335,7 @@ def test_suspicious_selector_jinja2_appdirs(self): STATIC_TEMPLATES={ "ENGINES": [ { - "BACKEND": "render_static.backends.StaticJinja2Templates", + "BACKEND": "render_static.backends.jinja2.StaticJinja2Templates", "OPTIONS": {"loader": StaticFileSystemLoader(STATIC_TEMP_DIR)}, } ], @@ -381,7 +387,7 @@ def test_generate(self): STATIC_TEMPLATES={ "ENGINES": [ { - "BACKEND": "render_static.backends.StaticJinja2Templates", + "BACKEND": "render_static.backends.jinja2.StaticJinja2Templates", "OPTIONS": {"loader": StaticFileSystemLoader(STATIC_TEMP_DIR)}, } ], @@ -410,7 +416,7 @@ def test_empty(self): STATIC_TEMPLATES={ "ENGINES": [ { - "BACKEND": "render_static.backends.StaticJinja2Templates", + "BACKEND": "render_static.backends.jinja2.StaticJinja2Templates", "OPTIONS": {"loader": StaticFileSystemLoader(STATIC_TEMP_DIR)}, } ], @@ -432,7 +438,7 @@ def test_none(self): STATIC_TEMPLATES={ "ENGINES": [ { - "BACKEND": "render_static.backends.StaticJinja2Templates", + "BACKEND": "render_static.backends.jinja2.StaticJinja2Templates", "OPTIONS": {"loader": StaticFileSystemLoader(STATIC_TEMP_DIR)}, } ], @@ -452,7 +458,7 @@ def test_one_tuple(self): STATIC_TEMPLATES={ "ENGINES": [ { - "BACKEND": "render_static.backends.StaticJinja2Templates", + "BACKEND": "render_static.backends.jinja2.StaticJinja2Templates", "OPTIONS": {"loader": StaticFileSystemLoader(STATIC_TEMP_DIR)}, } ], @@ -498,12 +504,12 @@ def test_mixed_list(self): STATIC_TEMPLATES={ "ENGINES": [ { - "BACKEND": "render_static.backends.StaticJinja2Templates", + "BACKEND": "render_static.backends.jinja2.StaticJinja2Templates", "OPTIONS": { "app_dir": "custom_templates", "loader": StaticDictLoader( { - "defines1.js": '{{ defines_to_js(classes, transpiler="render_static.DefaultDefineTranspiler", indent=" ") }}' + "defines1.js": '{{ defines_to_js(classes, transpiler="render_static.transpilers.DefaultDefineTranspiler", indent=" ") }}' "\nconsole.log(JSON.stringify(defines));", "defines2.js": "{{ defines_to_js(modules) }}" "\nconsole.log(JSON.stringify(defines));", @@ -522,13 +528,13 @@ def test_mixed_list(self): "context": { "classes": [ defines.MoreDefines, - "render_static.tests.defines.ExtendedDefines", + "tests.defines.ExtendedDefines", ] }, }, "defines2.js": { "dest": GLOBAL_STATIC_DIR / "defines2.js", - "context": {"modules": [defines, "render_static.tests.defines2"]}, + "context": {"modules": [defines, "tests.defines2"]}, }, "defines_error.js": { "dest": GLOBAL_STATIC_DIR / "defines_error.js", @@ -536,7 +542,7 @@ def test_mixed_list(self): }, "empty_defines.js": { "dest": GLOBAL_STATIC_DIR / "empty_defines.js", - "context": {"classes": ["render_static.tests.empty_defines"]}, + "context": {"classes": ["tests.empty_defines"]}, }, }, } @@ -546,22 +552,22 @@ class Jinja2DefinesToJavascriptTest(DefinesToJavascriptTest): STATIC_TEMPLATES={ "ENGINES": [ { - "BACKEND": "render_static.backends.StaticJinja2Templates", + "BACKEND": "render_static.backends.jinja2.StaticJinja2Templates", "OPTIONS": { "loader": StaticDictLoader( { "defines1.js": ( "{{" "defines_to_js([" - '"render_static.tests.defines.MoreDefines",' - '"render_static.tests.defines.ExtendedDefines"' + '"tests.defines.MoreDefines",' + '"tests.defines.ExtendedDefines"' '], indent=" ", level=1) }}' "\nconsole.log(JSON.stringify(defines));" ), "defines2.js": ( "{{ defines_to_js([" - '"render_static.tests.defines",' - '"render_static.tests.defines2"], indent=" ", level=1) }}' + '"tests.defines",' + '"tests.defines2"], indent=" ", level=1) }}' "\nconsole.log(JSON.stringify(defines));" ), } @@ -608,8 +614,8 @@ def test_tab_completion(self): @override_settings( INSTALLED_APPS=[ - "render_static.tests.chain", - "render_static.tests.spa", + "tests.chain", + "tests.spa", "render_static", "django.contrib.auth", "django.contrib.contenttypes", @@ -619,11 +625,11 @@ def test_tab_completion(self): "django.contrib.staticfiles", "django.contrib.admin", ], - ROOT_URLCONF="render_static.tests.urls_bug_13", + ROOT_URLCONF="tests.urls_bug_13", STATIC_TEMPLATES={ "ENGINES": [ { - "BACKEND": "render_static.backends.StaticJinja2Templates", + "BACKEND": "render_static.backends.jinja2.StaticJinja2Templates", "OPTIONS": { "loader": StaticDictLoader({"urls.js": ("{{ urls_to_js() }}")}) }, @@ -666,7 +672,7 @@ def test_urls_to_js(self): STATIC_TEMPLATES={ "ENGINES": [ { - "BACKEND": "render_static.backends.StaticJinja2Templates", + "BACKEND": "render_static.backends.jinja2.StaticJinja2Templates", "APP_DIRS": True, "OPTIONS": {"autoescape": False}, } @@ -693,7 +699,7 @@ def test_wildcards(self): STATIC_TEMPLATES={ "ENGINES": [ { - "BACKEND": "render_static.backends.StaticJinja2Templates", + "BACKEND": "render_static.backends.jinja2.StaticJinja2Templates", "APP_DIRS": True, "OPTIONS": {"autoescape": False}, } @@ -715,7 +721,7 @@ def test_wildcards2(self): STATIC_TEMPLATES={ "ENGINES": [ { - "BACKEND": "render_static.backends.StaticJinja2Templates", + "BACKEND": "render_static.backends.jinja2.StaticJinja2Templates", "APP_DIRS": True, "OPTIONS": {"autoescape": False}, } @@ -731,7 +737,7 @@ def tearDown(self): STATIC_TEMPLATES={ "ENGINES": [ { - "BACKEND": "render_static.backends.StaticJinja2Templates", + "BACKEND": "render_static.backends.jinja2.StaticJinja2Templates", "APP_DIRS": True, "OPTIONS": {"autoescape": False}, } diff --git a/render_static/tests/js_tests.py b/tests/test_js.py similarity index 95% rename from render_static/tests/js_tests.py rename to tests/test_js.py index 6e3d2b5..7babc55 100644 --- a/render_static/tests/js_tests.py +++ b/tests/test_js.py @@ -23,11 +23,15 @@ from django.utils.module_loading import import_string from render_static import placeholders -from render_static.tests import bad_pattern, defines -from render_static.tests.enum_app.enums import DependentEnum, IndependentEnum -from render_static.tests.enum_app.models import EnumTester -from render_static.tests.tests import ENUM_STATIC_DIR, GLOBAL_STATIC_DIR, BaseTestCase -from render_static.transpilers import CodeWriter +from tests import bad_pattern, defines +from tests.enum_app.enums import DependentEnum, IndependentEnum +from tests.enum_app.models import EnumTester +from tests.test_core import ( + ENUM_STATIC_DIR, + GLOBAL_STATIC_DIR, + BaseTestCase, +) +from render_static.transpilers.base import CodeWriter from render_static.transpilers.enums_to_js import IGNORED_ENUMS from render_static.transpilers.urls_to_js import ClassURLWriter @@ -66,7 +70,7 @@ def get_content(file): def get_url_mod(): - from render_static.tests import urls + from tests import urls return urls @@ -183,13 +187,13 @@ def get_members(cls): "context": { "classes": [ defines.MoreDefines, - "render_static.tests.defines.ExtendedDefines", + "tests.defines.ExtendedDefines", ] }, }, "defines2.js": { "dest": GLOBAL_STATIC_DIR / "defines2.js", - "context": {"modules": [defines, "render_static.tests.defines2"]}, + "context": {"modules": [defines, "tests.defines2"]}, }, "defines_error.js": { "dest": GLOBAL_STATIC_DIR / "defines_error.js", @@ -197,7 +201,7 @@ def get_members(cls): }, "empty_defines.js": { "dest": GLOBAL_STATIC_DIR / "empty_defines.js", - "context": {"classes": ["render_static.tests.empty_defines"]}, + "context": {"classes": ["tests.empty_defines"]}, }, }, } @@ -233,7 +237,7 @@ def test_classes_to_js(self): ( "render_static.loaders.StaticLocMemLoader", { - "defines1.js": '{% defines_to_js defines="render_static.tests.defines.ExtendedDefines" indent=" " %}' + "defines1.js": '{% defines_to_js defines="tests.defines.ExtendedDefines" indent=" " %}' "\nconsole.log(JSON.stringify(defines));" }, ) @@ -247,7 +251,7 @@ def test_classes_to_js(self): ) def test_single_class_to_js(self): call_command("renderstatic", "defines1.js") - from render_static.tests.defines import ExtendedDefines + from tests.defines import ExtendedDefines self.assertEqual( self.diff_classes( @@ -295,7 +299,7 @@ def test_empty_module_to_js(self): ( "render_static.loaders.StaticLocMemLoader", { - "defines2.js": '{% defines_to_js defines="render_static.tests.defines2" %}' + "defines2.js": '{% defines_to_js defines="tests.defines2" %}' "\nconsole.log(JSON.stringify(defines));", }, ) @@ -309,7 +313,7 @@ def test_empty_module_to_js(self): ) def test_single_module_to_js(self): call_command("renderstatic", "defines2.js") - from render_static.tests import defines2 + from tests import defines2 self.assertEqual( self.diff_modules( @@ -336,13 +340,13 @@ def test_classes_to_js_error(self): "defines1.js": ( "{% " 'defines_to_js defines="' - "render_static.tests.defines.MoreDefines " - 'render_static.tests.defines.ExtendedDefines"|split ' + "tests.defines.MoreDefines " + 'tests.defines.ExtendedDefines"|split ' 'indent=" " %}' "\nconsole.log(JSON.stringify(defines));" ), "defines2.js": ( - '{% defines_to_js defines="render_static.tests.defines render_static.tests.defines2"|split indent=" " %}' + '{% defines_to_js defines="tests.defines tests.defines2"|split indent=" " %}' "\nconsole.log(JSON.stringify(defines));" ), }, @@ -371,8 +375,8 @@ def test_split(self): self.diff_modules( js_file=GLOBAL_STATIC_DIR / "defines2.js", py_modules=[ - "render_static.tests.defines", - "render_static.tests.defines2", + "tests.defines", + "tests.defines2", ], ), {}, @@ -420,7 +424,7 @@ def test_split(self): "context": { "classes": [ defines.MoreDefines, - "render_static.tests.defines.ExtendedDefines", + "tests.defines.ExtendedDefines", ] }, } @@ -503,7 +507,7 @@ def test_define_overrides(self): "context": { "classes": [ defines.MoreDefines, - "render_static.tests.defines.ExtendedDefines", + "tests.defines.ExtendedDefines", ], "context_keys": [], }, @@ -602,11 +606,20 @@ def do_gen(): self.indent() yield f'urls{accessor_str}({"" if self.legacy_args else "{"}' self.indent() - yield f'{"" if self.legacy_args else "kwargs: "}' f'{json.dumps(kwargs, cls=BestEffortEncoder)}{"," if args or query else ""}' + yield ( + f'{"" if self.legacy_args else "kwargs: "}' + f'{json.dumps(kwargs, cls=BestEffortEncoder)}{"," if args or query else ""}' + ) if args: - yield f'{"" if self.legacy_args else "args: "}' f'{json.dumps(args, cls=BestEffortEncoder)}{"," if query else ""}' + yield ( + f'{"" if self.legacy_args else "args: "}' + f'{json.dumps(args, cls=BestEffortEncoder)}{"," if query else ""}' + ) if query: - yield f'{"" if self.legacy_args else "query: "}' f"{json.dumps(query, cls=BestEffortEncoder)}" + yield ( + f'{"" if self.legacy_args else "query: "}' + f"{json.dumps(query, cls=BestEffortEncoder)}" + ) self.outdent(2) yield f'{"" if self.legacy_args else "}"}));' if self.catch: @@ -729,7 +742,7 @@ def convert_idx_to_type(arr, idx, typ): ( "render_static.loaders.StaticLocMemLoader", { - "urls.js": 'var urls = {\n{% urls_to_js transpiler="render_static.SimpleURLWriter" %}};' + "urls.js": 'var urls = {\n{% urls_to_js transpiler="render_static.transpilers.SimpleURLWriter" %}};' }, ) ], @@ -759,7 +772,7 @@ def setUp(self): placeholders.register_unnamed_placeholders( "re_path_unnamed_solo", ["adf", 143], - app_name="bogus_app" # this should still work because all placeholders that match any + app_name="bogus_app", # this should still work because all placeholders that match any # criteria are tried ) # repeat for coverage @@ -777,7 +790,7 @@ def setUp(self): ( "render_static.loaders.StaticLocMemLoader", { - "urls.js": '{% urls_to_js transpiler="render_static.ClassURLWriter" %}' + "urls.js": '{% urls_to_js transpiler="render_static.transpilers.ClassURLWriter" %}' }, ) ], @@ -805,7 +818,7 @@ def test_full_url_dump_class_es6(self): ( "render_static.loaders.StaticLocMemLoader", { - "urls.js": '{% urls_to_js transpiler="render_static.ClassURLWriter" %}' + "urls.js": '{% urls_to_js transpiler="render_static.transpilers.ClassURLWriter" %}' }, ) ], @@ -836,7 +849,7 @@ def test_full_url_dump_class_es6_legacy_args(self): "render_static.loaders.StaticLocMemLoader", { "urls.js": "var urls = {\n" - '{% urls_to_js transpiler="render_static.SimpleURLWriter" include=include %}' + '{% urls_to_js transpiler="render_static.transpilers.SimpleURLWriter" include=include %}' "\n};" }, ) @@ -865,7 +878,7 @@ def test_admin_urls(self): ( "render_static.loaders.StaticLocMemLoader", { - "urls.js": '{% urls_to_js transpiler="render_static.ClassURLWriter" %}' + "urls.js": '{% urls_to_js transpiler="render_static.transpilers.ClassURLWriter" %}' }, ) ], @@ -893,7 +906,7 @@ def test_full_url_dump_class(self): ( "render_static.loaders.StaticLocMemLoader", { - "urls.js": '{% urls_to_js transpiler="render_static.ClassURLWriter" %}' + "urls.js": '{% urls_to_js transpiler="render_static.transpilers.ClassURLWriter" %}' }, ) ], @@ -923,7 +936,7 @@ def test_full_url_dump_class_legacy_args(self): ( "render_static.loaders.StaticLocMemLoader", { - "urls.js": 'const urls = {\n{% urls_to_js transpiler="render_static.SimpleURLWriter" %}};' + "urls.js": 'const urls = {\n{% urls_to_js transpiler="render_static.transpilers.SimpleURLWriter" %}};' }, ) ], @@ -951,7 +964,7 @@ def test_full_url_dump_es6(self): { "urls.js": """ const urls = { -{% urls_to_js transpiler="render_static.SimpleURLWriter" include="path_tst,re_path_mixed,app2:app1"|split:"," exclude="admin"|split raise_on_not_found=False%} +{% urls_to_js transpiler="render_static.transpilers.SimpleURLWriter" include="path_tst,re_path_mixed,app2:app1"|split:"," exclude="admin"|split raise_on_not_found=False%} {% override "re_path_mixed" %} return { 'qname': '{{qname}}', @@ -1025,7 +1038,7 @@ def test_simple_url_overrides(self): "render_static.loaders.StaticLocMemLoader", { "urls.js": """ -{% urls_to_js transpiler="render_static.ClassURLWriter" include="path_tst,re_path_mixed,app2:app1"|split:"," exclude="admin"|split raise_on_not_found=False%} +{% urls_to_js transpiler="render_static.transpilers.ClassURLWriter" include="path_tst,re_path_mixed,app2:app1"|split:"," exclude="admin"|split raise_on_not_found=False%} {% override "constructor" %} @@ -1183,7 +1196,7 @@ def test_class_url_overrides(self): ( "render_static.loaders.StaticLocMemLoader", { - "urls.js": 'var urls = {\n{% urls_to_js transpiler="render_static.SimpleURLWriter" %}};' + "urls.js": 'var urls = {\n{% urls_to_js transpiler="render_static.transpilers.SimpleURLWriter" %}};' }, ) ], @@ -1371,7 +1384,7 @@ def test_full_url_dump(self): { "urls.js": "var urls = {\n" "{% urls_to_js " - 'transpiler="render_static.SimpleURLWriter" ' + 'transpiler="render_static.transpilers.SimpleURLWriter" ' "include=include " "exclude=exclude " "%}};" @@ -1469,7 +1482,7 @@ def test_filtering(self): { "urls.js": "var urls = {\n" "{% urls_to_js " - 'transpiler="render_static.SimpleURLWriter" ' + 'transpiler="render_static.transpilers.SimpleURLWriter" ' "include=include " "exclude=exclude " "%}};" @@ -1563,7 +1576,7 @@ def test_filtering_excl_only(self): "urls.js": "var urls = {\n" "{% urls_to_js " "url_conf=url_mod " - 'transpiler="render_static.SimpleURLWriter" ' + 'transpiler="render_static.transpilers.SimpleURLWriter" ' "include=include " "exclude=exclude " "%}};" @@ -1654,8 +1667,8 @@ def test_filtering_null_ns_incl(self): { "urls.js": "var urls = {\n" "{% urls_to_js " - 'url_conf="render_static.tests.urls" ' - 'transpiler="render_static.SimpleURLWriter" ' + 'url_conf="tests.urls" ' + 'transpiler="render_static.transpilers.SimpleURLWriter" ' "include=include " "exclude=exclude " "%}};" @@ -1732,7 +1745,7 @@ def test_top_lvl_ns_incl(self): @override_settings( - ROOT_URLCONF="render_static.tests.urls2", + ROOT_URLCONF="tests.urls2", STATIC_TEMPLATES={ "ENGINES": [ { @@ -1744,7 +1757,7 @@ def test_top_lvl_ns_incl(self): { "urls.js": ( "{% urls_to_js " - 'transpiler="render_static.ClassURLWriter" ' + 'transpiler="render_static.transpilers.ClassURLWriter" ' "include=include " "%}" ) @@ -1787,7 +1800,7 @@ def test_no_default_registered(self): { "urls.js": ( "{% urls_to_js " - 'transpiler="render_static.ClassURLWriter" ' + 'transpiler="render_static.transpilers.ClassURLWriter" ' "include=include " "%}" ) @@ -1828,7 +1841,7 @@ def test_non_capturing_unnamed(self): { "urls.js": ( "{% urls_to_js " - 'transpiler="render_static.ClassURLWriter" ' + 'transpiler="render_static.transpilers.ClassURLWriter" ' "include=include " "%}" ) @@ -1863,7 +1876,7 @@ def test_named_unnamed_conflation1(self): self.compare("special", args=["third"]) @override_settings( - ROOT_URLCONF="render_static.tests.urls3", + ROOT_URLCONF="tests.urls3", STATIC_TEMPLATES={ "context": {"include": ["default"]}, "ENGINES": [ @@ -1876,7 +1889,7 @@ def test_named_unnamed_conflation1(self): { "urls.js": ( "{% urls_to_js " - 'transpiler="render_static.ClassURLWriter" ' + 'transpiler="render_static.transpilers.ClassURLWriter" ' "include=include " "%}" ) @@ -1911,7 +1924,7 @@ def test_named_unnamed_conflation2(self): self.compare("special", args=["third"]) @override_settings( - ROOT_URLCONF="render_static.tests.urls4", + ROOT_URLCONF="tests.urls4", STATIC_TEMPLATES={ "context": {"include": ["default"]}, "ENGINES": [ @@ -1922,7 +1935,7 @@ def test_named_unnamed_conflation2(self): ( "render_static.loaders.StaticLocMemLoader", { - "urls.js": '{% urls_to_js transpiler="render_static.ClassURLWriter" %}' + "urls.js": '{% urls_to_js transpiler="render_static.transpilers.ClassURLWriter" %}' }, ) ], @@ -1956,7 +1969,7 @@ def test_named_unnamed_conflation3(self): self.assertTrue(True) @override_settings( - ROOT_URLCONF="render_static.tests.urls5", + ROOT_URLCONF="tests.urls5", STATIC_TEMPLATES={ "context": {"include": ["default"]}, "ENGINES": [ @@ -1967,7 +1980,7 @@ def test_named_unnamed_conflation3(self): ( "render_static.loaders.StaticLocMemLoader", { - "urls.js": '{% urls_to_js transpiler="render_static.ClassURLWriter" %}' + "urls.js": '{% urls_to_js transpiler="render_static.transpilers.ClassURLWriter" %}' }, ) ], @@ -2009,7 +2022,7 @@ def test_nested_named_unnamed(self): "render_static.loaders.StaticLocMemLoader", { "urls.js": "{% urls_to_js " - 'transpiler="render_static.ClassURLWriter" ' + 'transpiler="render_static.transpilers.ClassURLWriter" ' "include=include %}" }, ) @@ -2055,7 +2068,7 @@ def test_named_unnamed_bad_mix(self): "render_static.loaders.StaticLocMemLoader", { "urls.js": "{% urls_to_js " - 'transpiler="render_static.ClassURLWriter" ' + 'transpiler="render_static.transpilers.ClassURLWriter" ' "include=include %}" }, ) @@ -2090,7 +2103,7 @@ def test_named_unnamed_bad_mix2(self): { "urls.js": ( "{% urls_to_js " - 'transpiler="render_static.ClassURLWriter" ' + 'transpiler="render_static.transpilers.ClassURLWriter" ' "include=include " "%}" ) @@ -2174,7 +2187,7 @@ def setUp(self): ( "render_static.loaders.StaticLocMemLoader", { - "urls.js": 'var urls = {\n{% urls_to_js transpiler="render_static.SimpleURLWriter" include=include %}};' + "urls.js": 'var urls = {\n{% urls_to_js transpiler="render_static.transpilers.SimpleURLWriter" include=include %}};' }, ) ], @@ -2217,7 +2230,7 @@ def test_no_placeholders(self): ( "render_static.loaders.StaticLocMemLoader", { - "urls.js": 'var urls = {\n{% urls_to_js transpiler="render_static.SimpleURLWriter" include=include %}};' + "urls.js": 'var urls = {\n{% urls_to_js transpiler="render_static.transpilers.SimpleURLWriter" include=include %}};' }, ) ], @@ -2259,7 +2272,7 @@ def test_no_unnamed_placeholders(self): ( "render_static.loaders.StaticLocMemLoader", { - "urls.js": 'var urls = {\n{% urls_to_js transpiler="render_static.SimpleURLWriter" include=include %}};' + "urls.js": 'var urls = {\n{% urls_to_js transpiler="render_static.transpilers.SimpleURLWriter" include=include %}};' }, ) ], @@ -2292,7 +2305,7 @@ def test_bad_only_bad_unnamed_placeholders(self): ( "render_static.loaders.StaticLocMemLoader", { - "urls.js": '{% urls_to_js transpiler="render_static.tests.js_tests.BadVisitor" %};' + "urls.js": '{% urls_to_js transpiler="tests.js_tests.BadVisitor" %};' }, ) ], @@ -2315,7 +2328,7 @@ def test_bad_visitor_type(self): ( "render_static.loaders.StaticLocMemLoader", { - "urls.js": 'var urls = {\n{% urls_to_js transpiler="render_static.SimpleURLWriter" url_conf=url_mod %}};' + "urls.js": 'var urls = {\n{% urls_to_js transpiler="render_static.transpilers.SimpleURLWriter" url_conf=url_mod %}};' }, ) ], @@ -2345,7 +2358,7 @@ def test_no_urlpatterns(self): ( "render_static.loaders.StaticLocMemLoader", { - "urls.js": 'var urls = {\n{% urls_to_js transpiler="render_static.SimpleURLWriter" url_conf=url_mod %}};' + "urls.js": 'var urls = {\n{% urls_to_js transpiler="render_static.transpilers.SimpleURLWriter" url_conf=url_mod %}};' }, ) ], @@ -2404,13 +2417,13 @@ def setUp(self): "include=include " "%}", "urls3.js": "var urls = {\n{% urls_to_js " - 'transpiler="render_static.SimpleURLWriter" ' + 'transpiler="render_static.transpilers.SimpleURLWriter" ' "raise_on_not_found=False " "indent=None " "include=include " "%}}\n", "urls4.js": "var urls = {\n{% urls_to_js " - 'transpiler="render_static.SimpleURLWriter" ' + 'transpiler="render_static.transpilers.SimpleURLWriter" ' "raise_on_not_found=True " 'indent="" ' "include=include " @@ -2534,7 +2547,7 @@ def tearDown(self): @override_settings( - ROOT_URLCONF="render_static.tests.js_reverse_urls", + ROOT_URLCONF="tests.js_reverse_urls", STATIC_TEMPLATES={ "ENGINES": [ { @@ -2545,7 +2558,7 @@ def tearDown(self): "render_static.loaders.StaticLocMemLoader", { "urls.js": "{% urls_to_js " - 'transpiler="render_static.ClassURLWriter" ' + 'transpiler="render_static.transpilers.ClassURLWriter" ' 'exclude="exclude_namespace"|split ' "%};" }, @@ -2578,7 +2591,7 @@ def test_js_reverse_urls(self): @override_settings( - ROOT_URLCONF="render_static.tests.urls_precedence", + ROOT_URLCONF="tests.urls_precedence", STATIC_TEMPLATES={ "ENGINES": [ { @@ -2589,7 +2602,7 @@ def test_js_reverse_urls(self): "render_static.loaders.StaticLocMemLoader", { "urls.js": "{% urls_to_js " - 'transpiler="render_static.ClassURLWriter" ' + 'transpiler="render_static.transpilers.ClassURLWriter" ' 'exclude="exclude_namespace"|split ' "%};" }, @@ -2627,7 +2640,7 @@ def test_js_reverse_urls(self): @override_settings( - ROOT_URLCONF="render_static.tests.urls_bug_65", + ROOT_URLCONF="tests.urls_bug_65", STATIC_TEMPLATES={ "ENGINES": [ { @@ -2639,7 +2652,7 @@ def test_js_reverse_urls(self): { "urls.js": ( "{% urls_to_js " - 'transpiler="render_static.ClassURLWriter" %}' + 'transpiler="render_static.transpilers.ClassURLWriter" %}' ) }, ) @@ -2725,8 +2738,8 @@ def test_bug_65_compiles(self): @override_settings( INSTALLED_APPS=[ - "render_static.tests.chain", - "render_static.tests.spa", + "tests.chain", + "tests.spa", "render_static", "django.contrib.auth", "django.contrib.contenttypes", @@ -2736,7 +2749,7 @@ def test_bug_65_compiles(self): "django.contrib.staticfiles", "django.contrib.admin", ], - ROOT_URLCONF="render_static.tests.urls_bug_13", + ROOT_URLCONF="tests.urls_bug_13", STATIC_TEMPLATES={ "ENGINES": [ { @@ -2748,7 +2761,7 @@ def test_bug_65_compiles(self): { "urls.js": ( "{% urls_to_js " - 'transpiler="render_static.ClassURLWriter" %}' + 'transpiler="render_static.transpilers.ClassURLWriter" %}' ) }, ) @@ -2795,7 +2808,7 @@ def test_bug_13_multilevel_args(self): @override_settings( INSTALLED_APPS=[ - "render_static.tests.spa", + "tests.spa", "render_static", "django.contrib.auth", "django.contrib.contenttypes", @@ -2805,7 +2818,7 @@ def test_bug_13_multilevel_args(self): "django.contrib.staticfiles", "django.contrib.admin", ], - ROOT_URLCONF="render_static.tests.spa_urls", + ROOT_URLCONF="tests.spa_urls", STATICFILES_DIRS=[ ("spa", GLOBAL_STATIC_DIR), ], @@ -2979,7 +2992,7 @@ def get_js_structure(self, js_file): # pragma: no cover @override_settings( INSTALLED_APPS=[ - "render_static.tests.enum_app", + "tests.enum_app", "render_static", "django.contrib.auth", "django.contrib.contenttypes", @@ -2989,7 +3002,7 @@ def get_js_structure(self, js_file): # pragma: no cover "django.contrib.staticfiles", "django.contrib.admin", ], - ROOT_URLCONF="render_static.tests.enum_app.urls", + ROOT_URLCONF="tests.enum_app.urls", STATIC_TEMPLATES={ "context": { "include_properties": True, @@ -3014,7 +3027,7 @@ def get_js_structure(self, js_file): # pragma: no cover ( "enum_app/test.js", { - "context": {"enums": "render_static.tests.enum_app.defines.Def"}, + "context": {"enums": "tests.enum_app.defines.Def"}, "dest": GLOBAL_STATIC_DIR / "enum/def.js", }, ), @@ -3023,7 +3036,7 @@ def get_js_structure(self, js_file): # pragma: no cover ) class EnumGeneratorTest(EnumComparator, BaseTestCase): def test_simple(self): - from render_static.tests.enum_app.defines import Def + from tests.enum_app.defines import Def call_command("renderstatic", "enum_app/test.js") self.enums_compare( @@ -3042,7 +3055,7 @@ def test_enum_properties(self): ) @override_settings( - ROOT_URLCONF="render_static.tests.enum_app.urls", + ROOT_URLCONF="tests.enum_app.urls", STATIC_TEMPLATES={ "context": { "include_properties": True, @@ -3055,7 +3068,7 @@ def test_enum_properties(self): "enum_app/test.js", { "context": { - "enums": "render_static.tests.enum_app.models.EnumTester", + "enums": "tests.enum_app.models.EnumTester", "test_enums": [ EnumTester.MapBoxStyle, EnumTester.AddressRoute, @@ -3090,18 +3103,14 @@ def test_model_import_string(self): "templates": [ ( "enum_app/test.js", - { - "context": { - "enums": ["render_static.tests.enum_app.defines.TimeEnum"] - } - }, + {"context": {"enums": ["tests.enum_app.defines.TimeEnum"]}}, ) ], } ) def test_datetime_enum(self): call_command("renderstatic", "enum_app/test.js") - from render_static.tests.enum_app.defines import TimeEnum + from tests.enum_app.defines import TimeEnum self.enums_compare( js_file=ENUM_STATIC_DIR / "enum_app/test.js", @@ -3132,11 +3141,7 @@ def test_datetime_enum(self): "templates": [ ( "enum_app/test.js", - { - "context": { - "enums": ["render_static.tests.enum_app.defines.TimeEnum"] - } - }, + {"context": {"enums": ["tests.enum_app.defines.TimeEnum"]}}, ), ], } @@ -3145,7 +3150,7 @@ def test_datetime_enum_to_javascript_param(self): call_command("renderstatic", "enum_app/test.js") from dateutil.parser import parse - from render_static.tests.enum_app.defines import TimeEnum + from tests.enum_app.defines import TimeEnum times = run_js_file(GLOBAL_STATIC_DIR / "enum_app/test.js").split() self.assertEqual(parse(times[0]).date(), TimeEnum.YEAR1.value) @@ -3401,7 +3406,7 @@ def test_export_on_and_classname(self): ], "builtins": [ "render_static.templatetags.render_static", - "render_static.tests.enum_app.templatetags.enum_test", + "tests.enum_app.templatetags.enum_test", ], }, } @@ -3449,7 +3454,7 @@ def test_export_off_and_classname(self): ], "builtins": [ "render_static.templatetags.render_static", - "render_static.tests.enum_app.templatetags.enum_test", + "tests.enum_app.templatetags.enum_test", ], }, } @@ -3749,7 +3754,7 @@ def test_raise_on_not_found(self): ( "render_static.loaders.StaticLocMemLoader", { - "enum_app/test.js": '{% enums_to_js enums=enums symmetric_properties=True transpiler="render_static.EnumClassWriter" %}\n' + "enum_app/test.js": '{% enums_to_js enums=enums symmetric_properties=True transpiler="render_static.transpilers.EnumClassWriter" %}\n' 'try { AddressRoute.get("Aly") } catch (e) {console.log(JSON.stringify({not_found: e instanceof TypeError ? "TypeError" : "Unknown"}));}' }, ) @@ -3823,7 +3828,7 @@ def test_exclude_ignored_and_no_repeat(self): self.assertNotIn(f"class {en.__name__}", contents) @override_settings( - ROOT_URLCONF="render_static.tests.enum_app.urls", + ROOT_URLCONF="tests.enum_app.urls", STATIC_TEMPLATES={ "context": { "include_properties": True, @@ -3879,7 +3884,7 @@ def test_chained_enum_values_missing_dep(self): ) @override_settings( - ROOT_URLCONF="render_static.tests.enum_app.urls", + ROOT_URLCONF="tests.enum_app.urls", STATIC_TEMPLATES={ "context": { "include_properties": True, @@ -3943,9 +3948,9 @@ def test_chained_enum_values(self): "render_static.loaders.StaticLocMemLoader", { "enum_app/test.js": """ -{% enums_to_js enums='render_static.tests.enum_app.models.EnumTester.Color' include_properties="hex,name,value,rgb,label"|split:"," %} -{% enums_to_js enums='render_static.tests.enum_app.models.EnumTester.MapBoxStyle' include_properties="value,name,label,uri,slug,version"|split:"," %} -{% enums_to_js enums='render_static.tests.enum_app.models.EnumTester.AddressRoute' include_properties="name,value,alt"|split:"," %} +{% enums_to_js enums='tests.enum_app.models.EnumTester.Color' include_properties="hex,name,value,rgb,label"|split:"," %} +{% enums_to_js enums='tests.enum_app.models.EnumTester.MapBoxStyle' include_properties="value,name,label,uri,slug,version"|split:"," %} +{% enums_to_js enums='tests.enum_app.models.EnumTester.AddressRoute' include_properties="name,value,alt"|split:"," %} """ }, ) @@ -4143,8 +4148,8 @@ def test_overrides(self): "render_static.loaders.StaticLocMemLoader", { "enum_app/test.js": """ -{% enums_to_js enums="render_static.tests.enum_app.models.EnumTester.MapBoxStyle" %} -{% enums_to_js enums="render_static.tests.enum_app.models.EnumTester.AddressRoute" symmetric_properties=True %} +{% enums_to_js enums="tests.enum_app.models.EnumTester.MapBoxStyle" %} +{% enums_to_js enums="tests.enum_app.models.EnumTester.AddressRoute" symmetric_properties=True %} {% override "testContext" %} static testContext() { @@ -4160,7 +4165,7 @@ def test_overrides(self): } {% endoverride %} {% endenums_to_js %} -{% transpile "render_static.tests.enum_app.models.EnumTester.Color" "render_static.EnumClassWriter" %} +{% transpile "tests.enum_app.models.EnumTester.Color" "render_static.transpilers.EnumClassWriter" %} console.log(JSON.stringify( { testContext: AddressRoute.testContext(), @@ -4204,7 +4209,7 @@ def test_multi_block(self): ( "render_static.loaders.StaticLocMemLoader", { - "enum_app/test.js": "{% enums_to_js enums='render_static.tests.enum_app.models.EnumTester.Nope' %}" + "enum_app/test.js": "{% enums_to_js enums='tests.enum_app.models.EnumTester.Nope' %}" }, ), ( @@ -4222,7 +4227,7 @@ def test_multi_block(self): ( "render_static.loaders.StaticLocMemLoader", { - "enum_app/test4.js": "{% enums_to_js enums='render_static.tests.enum_app.models.DNE' %}" + "enum_app/test4.js": "{% enums_to_js enums='tests.enum_app.models.DNE' %}" }, ), ], @@ -4244,7 +4249,7 @@ def test_import_error(self): @override_settings( - ROOT_URLCONF="render_static.tests.urls_default_args", + ROOT_URLCONF="tests.urls_default_args", STATIC_TEMPLATES={ "ENGINES": [ { @@ -4289,7 +4294,7 @@ def test_sitemap_url_generation(self): "complex_default", Path(GLOBAL_STATIC_DIR / "urls.js").read_text() ) - from render_static.tests.urls_default_args import Default + from tests.urls_default_args import Default self.assertEqual( self.get_url_from_js( diff --git a/render_static/tests/traverse_tests.py b/tests/test_traversal.py similarity index 90% rename from render_static/tests/traverse_tests.py rename to tests/test_traversal.py index a0f585e..1b4e53e 100644 --- a/render_static/tests/traverse_tests.py +++ b/tests/test_traversal.py @@ -1,13 +1,14 @@ """ Tests for base transpiler source tree traversal. """ + from copy import copy from enum import Enum, auto from types import ModuleType from django.test import TestCase -from render_static.transpilers import Transpiler +from render_static.transpilers.base import Transpiler from render_static.transpilers.enums_to_js import IGNORED_ENUMS @@ -110,7 +111,7 @@ def visit(self, target, is_last, is_final): class TranspileTraverseTests(TestCase): def test_class_traversal(self): - from render_static.tests.traverse.module1 import Class1Module1 + from tests.traverse.module1 import Class1Module1 transpiler = TranspilerTester() transpiler.transpile([Class1Module1]) @@ -143,10 +144,10 @@ def test_class_traversal(self): ) def test_class_import_string_traversal(self): - from render_static.tests.traverse.module1 import Class1Module1 + from tests.traverse.module1 import Class1Module1 transpiler = TranspilerTester() - transpiler.transpile(["render_static.tests.traverse.module1.Class1Module1"]) + transpiler.transpile(["tests.traverse.module1.Class1Module1"]) self.assertEqual( transpiler.TRAVERSAL, [ @@ -176,12 +177,10 @@ def test_class_import_string_traversal(self): ) def test_deduplicate_class(self): - from render_static.tests.traverse.module1 import Class1Module1 + from tests.traverse.module1 import Class1Module1 transpiler = TranspilerTester() - transpiler.transpile( - [Class1Module1, "render_static.tests.traverse.module1.Class1Module1"] - ) + transpiler.transpile([Class1Module1, "tests.traverse.module1.Class1Module1"]) self.assertEqual( transpiler.TRAVERSAL, [ @@ -211,8 +210,8 @@ def test_deduplicate_class(self): ) def test_module_traversal(self): - from render_static.tests.traverse import module1 - from render_static.tests.traverse.module1 import Class1Module1, Class2Module1 + from tests.traverse import module1 + from tests.traverse.module1 import Class1Module1, Class2Module1 transpiler = TranspilerTester() transpiler.transpile([module1]) @@ -272,11 +271,11 @@ def test_module_traversal(self): ) def test_module_import_string_traversal(self): - from render_static.tests.traverse import module1 - from render_static.tests.traverse.module1 import Class1Module1, Class2Module1 + from tests.traverse import module1 + from tests.traverse.module1 import Class1Module1, Class2Module1 transpiler = TranspilerTester() - transpiler.transpile(["render_static.tests.traverse.module1"]) + transpiler.transpile(["tests.traverse.module1"]) self.assertEqual( transpiler.TRAVERSAL, [ @@ -333,11 +332,11 @@ def test_module_import_string_traversal(self): ) def test_deduplicate_module(self): - from render_static.tests.traverse import module1 - from render_static.tests.traverse.module1 import Class1Module1, Class2Module1 + from tests.traverse import module1 + from tests.traverse.module1 import Class1Module1, Class2Module1 transpiler = TranspilerTester() - transpiler.transpile(["render_static.tests.traverse.module1", module1]) + transpiler.transpile(["tests.traverse.module1", module1]) self.assertEqual( transpiler.TRAVERSAL, [ @@ -394,16 +393,16 @@ def test_deduplicate_module(self): ) def test_multi_module_traversal(self): - from render_static.tests.traverse import module1 - from render_static.tests.traverse.module1 import Class1Module1, Class2Module1 - from render_static.tests.traverse.sub_pkg import module2 - from render_static.tests.traverse.sub_pkg.module2 import ( + from tests.traverse import module1 + from tests.traverse.module1 import Class1Module1, Class2Module1 + from tests.traverse.sub_pkg import module2 + from tests.traverse.sub_pkg.module2 import ( Class1Module2, Class2Module2, ) transpiler = TranspilerTester() - transpiler.transpile(["render_static.tests.traverse.module1", module2]) + transpiler.transpile(["tests.traverse.module1", module2]) expected = [ EnterModule(module1, False, False), Visit(module1, False, False), @@ -482,7 +481,7 @@ def test_multi_module_traversal(self): self.assertEqual(transpiler.TRAVERSAL, expected) def test_multi_class_traversal(self): - from render_static.tests.traverse.module1 import Class1Module1, Class2Module1 + from tests.traverse.module1 import Class1Module1, Class2Module1 transpiler = TranspilerTester() transpiler.transpile([Class1Module1, Class2Module1]) @@ -532,12 +531,12 @@ def include_target(self, target): print(f"{target}: {ret}") return ret - from render_static.tests.traverse import module1 - from render_static.tests.traverse.module1 import Class1Module1, Class2Module1 - from render_static.tests.traverse.sub_pkg.module2 import Class2Module2 + from tests.traverse import module1 + from tests.traverse.module1 import Class1Module1, Class2Module1 + from tests.traverse.sub_pkg.module2 import Class2Module2 transpiler = EnumTranspiler() - transpiler.transpile(["render_static.tests.traverse.module1", Class2Module2]) + transpiler.transpile(["tests.traverse.module1", Class2Module2]) expected = [ EnterModule(module1, False, False), EnterClass(Class1Module1, False, False), @@ -578,30 +577,30 @@ def test_app_transpile_string(self): from django.apps import apps transpiler = TranspilerTester() - transpiler.transpile(["render_static.tests.enum_app"]) + transpiler.transpile(["tests.enum_app"]) self.assertEqual( transpiler.TRAVERSAL, - [Visit(apps.get_app_config("render_static_tests_enum_app"), True, True)], + [Visit(apps.get_app_config("tests_enum_app"), True, True)], ) def test_app_transpile_app_config(self): from django.apps import apps transpiler = TranspilerTester() - transpiler.transpile([apps.get_app_config("render_static_tests_enum_app")]) + transpiler.transpile([apps.get_app_config("tests_enum_app")]) self.assertEqual( transpiler.TRAVERSAL, - [Visit(apps.get_app_config("render_static_tests_enum_app"), True, True)], + [Visit(apps.get_app_config("tests_enum_app"), True, True)], ) def test_app_transpile_app_label(self): from django.apps import apps transpiler = TranspilerTester() - transpiler.transpile(["render_static_tests_enum_app"]) + transpiler.transpile(["tests_enum_app"]) self.assertEqual( transpiler.TRAVERSAL, - [Visit(apps.get_app_config("render_static_tests_enum_app"), True, True)], + [Visit(apps.get_app_config("tests_enum_app"), True, True)], ) def test_deduplicate_appconfigs(self): @@ -610,14 +609,14 @@ def test_deduplicate_appconfigs(self): transpiler = TranspilerTester() transpiler.transpile( [ - "render_static.tests.enum_app", - apps.get_app_config("render_static_tests_enum_app"), - "render_static_tests_enum_app", + "tests.enum_app", + apps.get_app_config("tests_enum_app"), + "tests_enum_app", ] ) self.assertEqual( transpiler.TRAVERSAL, - [Visit(apps.get_app_config("render_static_tests_enum_app"), True, True)], + [Visit(apps.get_app_config("tests_enum_app"), True, True)], ) def test_transpiled_parent_and_children(self): @@ -629,9 +628,9 @@ def include_defines(target): return False transpiler = TranspilerTester(include=include_defines) - from render_static.tests.traverse import models + from tests.traverse import models - transpiler.transpile(["render_static.tests.traverse.models", None]) + transpiler.transpile(["tests.traverse.models", None]) self.assertEqual( transpiler.TRAVERSAL, [ diff --git a/tests/test_web.py b/tests/test_web.py new file mode 100644 index 0000000..d1b3bdd --- /dev/null +++ b/tests/test_web.py @@ -0,0 +1,163 @@ +""" +Single Page Application (SPA) tests. These tests test several patterns that +allow apps using urls_to_js to be included more than once and under different +namespaces. These patterns incur a dependency on renderstatic to any +users of the SPA apps. See Runtimes in the documentation for more details. +""" + +import sys +import json +import logging +import os + +import pytest + +import time +from django.core.management import call_command +from django.test import LiveServerTestCase, override_settings +from django.urls import reverse +from selenium.webdriver.common.by import By +from contextlib import contextmanager + +from tests.test_core import LOCAL_STATIC_DIR, BaseTestCase + +logger = logging.getLogger(__name__) + + +@contextmanager +def web_driver(width=1920, height=1200): + import platform + from selenium import webdriver + from selenium.webdriver.chrome.options import Options as ChromeOptions + + # Set up headless browser options + def opts(options=ChromeOptions()): + options.add_argument("--headless") + options.add_argument("--no-sandbox") + options.add_argument("--disable-dev-shm-usage") + options.add_argument("--disable-gpu") + options.add_argument(f"--window-size={width}x{height}") + return options + + def chrome(): + from selenium.webdriver.chrome.service import Service + from webdriver_manager.chrome import ChromeDriverManager + + return webdriver.Chrome( + service=Service(ChromeDriverManager().install()), options=opts() + ) + + def chromium(): + from selenium.webdriver.chrome.service import Service as ChromiumService + from webdriver_manager.chrome import ChromeDriverManager + from webdriver_manager.core.os_manager import ChromeType + + return webdriver.Chrome( + service=ChromiumService( + ChromeDriverManager(chrome_type=ChromeType.CHROMIUM).install() + ), + options=opts(), + ) + + def firefox(): + from selenium import webdriver + from selenium.webdriver.firefox.options import Options + from selenium.webdriver.firefox.service import Service as FirefoxService + from webdriver_manager.firefox import GeckoDriverManager + + return webdriver.Firefox( + service=FirefoxService(GeckoDriverManager().install()), + options=opts(Options()), + ) + + def edge(): + from selenium.webdriver.edge.options import Options + from selenium.webdriver.edge.service import Service as EdgeService + from webdriver_manager.microsoft import EdgeChromiumDriverManager + + options = Options() + options.use_chromium = True + return webdriver.Edge( + service=EdgeService(EdgeChromiumDriverManager().install()), + options=opts(options), + ) + + services = [ + chrome, + edge if platform.system().lower() == "windows" else chromium, + firefox, + ] + + driver = None + for service in services: + try: + driver = service() + break # use the first one that works! + except Exception as err: + pass + + if driver: + yield driver + driver.quit() + else: + raise RuntimeError("Unable to initialize any webdriver.") + + +@pytest.mark.skipif(sys.version_info < (3, 9), reason="Requires Python 3.9 or higher") +@override_settings( + INSTALLED_APPS=[ + "tests.spa", + "render_static", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.sites", + "django.contrib.messages", + "django.contrib.staticfiles", + "django.contrib.admin", + ], + ROOT_URLCONF="tests.spa_urls", + STATICFILES_DIRS=[ + ("spa", LOCAL_STATIC_DIR), + ], + STATIC_TEMPLATES={ + "templates": { + "spa/urls.js": { + "context": {"include": ["spa1", "spa2"]}, + "dest": str(LOCAL_STATIC_DIR / "urls.js"), + } + } + }, +) +class TestMultipleURLTreeSPAExample(BaseTestCase, LiveServerTestCase): + def setUp(self): + os.makedirs(LOCAL_STATIC_DIR, exist_ok=True) + call_command("renderstatic", "spa/urls.js", "--traceback") + call_command("collectstatic") + + def test_example_pattern(self): + with web_driver() as driver: + driver.get(f'{self.live_server_url}{reverse("spa1:index")}') + time.sleep(2) + elem = driver.find_element(By.ID, "qry-result") + text = str(elem.text) + js = json.loads(text) + self.assertEqual(js["request"], "/spa1/qry/") + elem = driver.find_element(By.ID, "qry-result-arg") + text = str(elem.text) + js = json.loads(text) + self.assertEqual(js["request"], "/spa1/qry/5") + + driver.get(f'{self.live_server_url}{reverse("spa2:index")}') + time.sleep(2) + elem = driver.find_element(By.ID, "qry-result") + text = str(elem.text) + js = json.loads(text) + self.assertEqual(js["request"], "/spa2/qry/") + elem = driver.find_element(By.ID, "qry-result-arg") + text = str(elem.text) + js = json.loads(text) + self.assertEqual(js["request"], "/spa2/qry/5") + + # def tearDown(self): + # pass diff --git a/render_static/tests/yaml_tests.py b/tests/test_yaml.py similarity index 97% rename from render_static/tests/yaml_tests.py rename to tests/test_yaml.py index 754686f..6bb234f 100644 --- a/render_static/tests/yaml_tests.py +++ b/tests/test_yaml.py @@ -6,7 +6,7 @@ from django.core.management import call_command from django.test import override_settings -from render_static.tests.tests import ( +from tests.test_core import ( APP1_STATIC_DIR, EXPECTED_DIR, BaseTestCase, diff --git a/tests/traverse/__init__.py b/tests/traverse/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/render_static/tests/traverse/models.py b/tests/traverse/models.py similarity index 100% rename from render_static/tests/traverse/models.py rename to tests/traverse/models.py diff --git a/render_static/tests/traverse/module1.py b/tests/traverse/module1.py similarity index 100% rename from render_static/tests/traverse/module1.py rename to tests/traverse/module1.py diff --git a/render_static/tests/traverse/sub_pkg/__init__.py b/tests/traverse/sub_pkg/__init__.py similarity index 100% rename from render_static/tests/traverse/sub_pkg/__init__.py rename to tests/traverse/sub_pkg/__init__.py diff --git a/render_static/tests/traverse/sub_pkg/module2.py b/tests/traverse/sub_pkg/module2.py similarity index 100% rename from render_static/tests/traverse/sub_pkg/module2.py rename to tests/traverse/sub_pkg/module2.py diff --git a/render_static/tests/urls.py b/tests/urls.py similarity index 75% rename from render_static/tests/urls.py rename to tests/urls.py index 895a901..0696a5c 100644 --- a/render_static/tests/urls.py +++ b/tests/urls.py @@ -8,12 +8,12 @@ path("test/simple/", TestView.as_view(), name="path_tst"), path("test/simple/", TestView.as_view(), name="path_tst"), path("test/different//", TestView.as_view(), name="path_tst"), - path("sub1/", include("render_static.tests.app1.urls", namespace="sub1")), - path("sub2/", include("render_static.tests.app1.urls", namespace="sub2")), - path("sub3/", include("render_static.tests.app2.urls")), + path("sub1/", include("tests.app1.urls", namespace="sub1")), + path("sub2/", include("tests.app1.urls", namespace="sub2")), + path("sub3/", include("tests.app2.urls")), # django doesnt support inclusions with additional arguments - # path('/sub2/', include('render_static.tests.app1.urls', namespace='sub2')), - path("", include("render_static.tests.app3.urls")), # included into the default ns + # path('/sub2/', include('tests.app1.urls', namespace='sub2')), + path("", include("tests.app3.urls")), # included into the default ns re_path(r"^re_path/[adfa]{2,3}$", TestView.as_view(), name="re_path_tst"), re_path(r"^re_path/(?P\w+)/$", TestView.as_view(), name="re_path_tst"), re_path( diff --git a/render_static/tests/urls2.py b/tests/urls2.py similarity index 100% rename from render_static/tests/urls2.py rename to tests/urls2.py diff --git a/render_static/tests/urls3.py b/tests/urls3.py similarity index 100% rename from render_static/tests/urls3.py rename to tests/urls3.py diff --git a/render_static/tests/urls4.py b/tests/urls4.py similarity index 100% rename from render_static/tests/urls4.py rename to tests/urls4.py diff --git a/render_static/tests/urls5.py b/tests/urls5.py similarity index 100% rename from render_static/tests/urls5.py rename to tests/urls5.py diff --git a/tests/urls_bug_13.py b/tests/urls_bug_13.py new file mode 100644 index 0000000..b228fee --- /dev/null +++ b/tests/urls_bug_13.py @@ -0,0 +1,19 @@ +""" +Reproduce: https://github.com/bckohan/django-render-static/issues/65 +""" + +from django.urls import include, path, re_path + +urlpatterns = [ + path("spa1//", include("tests.spa.urls", namespace="spa1")), + path("spa2/", include("tests.spa.urls", namespace="spa2")), + path("multi//", include("tests.chain.urls")), + re_path( + r"^multi/(?P\w+)/", + include("tests.chain.urls", namespace="chain_re"), + ), + path( + "noslash/", + include("tests.chain.urls", namespace="noslash"), + ), +] diff --git a/render_static/tests/urls_bug_65.py b/tests/urls_bug_65.py similarity index 99% rename from render_static/tests/urls_bug_65.py rename to tests/urls_bug_65.py index 7a8612f..7748b08 100644 --- a/render_static/tests/urls_bug_65.py +++ b/tests/urls_bug_65.py @@ -1,6 +1,7 @@ """ Reproduce: https://github.com/bckohan/django-render-static/issues/65 """ + from django.urls import path, re_path from .views import TestView diff --git a/render_static/tests/urls_default_args.py b/tests/urls_default_args.py similarity index 93% rename from render_static/tests/urls_default_args.py rename to tests/urls_default_args.py index f81aa70..f34bd41 100644 --- a/render_static/tests/urls_default_args.py +++ b/tests/urls_default_args.py @@ -3,7 +3,7 @@ from django.urls import include, path from django.utils.timezone import now -from render_static.tests.views import TestView +from tests.views import TestView class BlogSitemap(Sitemap): diff --git a/render_static/tests/urls_precedence.py b/tests/urls_precedence.py similarity index 100% rename from render_static/tests/urls_precedence.py rename to tests/urls_precedence.py diff --git a/render_static/tests/views.py b/tests/views.py similarity index 100% rename from render_static/tests/views.py rename to tests/views.py