Compare commits

...
11 Commits
Author SHA1 Message Date
truePhilipp 788308cf44 moving along
Stylecheck / stylecheck (push) Failing after 9s
Tests / unittest (3.12) (push) Successful in 15s
Stylecheck / stylecheck-tests (push) Successful in 21s
2026-10-06 15:23:27 +02:00
truePhilipp f61305d1b0 added the new attributes to Attributes.md
Stylecheck / stylecheck (push) Failing after 2m8s
Stylecheck / stylecheck-tests (push) Failing after 2m9s
Tests / unittest (3.12) (push) Successful in 2m10s
2024-05-14 22:35:10 +02:00
truePhilipp 8ac332de49 added more keywords to pyproject.toml 2024-05-14 22:18:16 +02:00
truePhilipp ddc9add551 added assets_formatter and remove_formatter 2024-05-14 22:13:30 +02:00
truePhilipp 8697f3ecb5 added pip installation text 2024-05-11 01:44:10 +02:00
truePhilipp 530bf592a8 corrected environment name 2024-05-11 01:35:29 +02:00
truePhilipp 2dc16547dd enable pypi release 2024-05-11 01:33:06 +02:00
truePhilipp 3546ac40f9 removed main from deploy.yml 2024-05-11 01:19:53 +02:00
truePhilipp a274f53b6e dynamic version test 2024-05-11 01:18:00 +02:00
truePhilipp eef7756e5b added more attribute explanations to Attributes.md 2024-05-11 00:23:07 +02:00
truePhilipp beba63967b created TestDirectory context manager (typing is disabled as i can't find a fix for the generic typing error) 2024-05-10 11:40:30 +00:00
13 changed files with 254 additions and 74 deletions

No files matched your search

+45
View File
@@ -0,0 +1,45 @@
name: Deploy
on:
push:
tags:
- 'v*.*.*'
pull_request:
tags:
- 'v*.*.*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Python 3.12
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install Dependencies
run: pip install ".[dev]"
- name: Build on Python 3.12
run: hatch build
- name: Store the distribution packages
uses: actions/upload-artifact@v3
with:
name: python-package-distributions
path: dist/
publish-to-pypi:
needs:
- build
runs-on: ubuntu-latest
environment:
name: Deployment
url: https://pypi.org/p/html_to_django
permissions:
id-token: write
steps:
- name: Download all the dists
uses: actions/download-artifact@v3
with:
name: python-package-distributions
path: dist/
- name: Publish distribution to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
+3 -1
View File
@@ -7,7 +7,9 @@ Converts HTML files with special attributes to Django templates.
# Installation # Installation
TODO: pip install instructions ```bash
pip install html-to-django
```
# Usage # Usage
+64 -1
View File
@@ -1,5 +1,11 @@
# Attributes # Attributes
| dj-assets | Modify the "href" or "src" attributes of elements with a path prefix and wrap them in a Django static command. |
|----------------|----------------------------------------------------------------------------------------------------------------|
| Syntax | `<html dj-assets="asset_identifier;asset_prefix">...</html>` |
| Example Input | `<html dj-assets="assets;module"><img src="assets/image.png"></html>` |
| Example Output | `<html><img src="{% static 'module/assets/image.png' %}"/></html>` |
| dj-attr | Dynamically assign attributes using Django template variables. | | dj-attr | Dynamically assign attributes using Django template variables. |
|----------------|---------------------------------------------------------------------------------------| |----------------|---------------------------------------------------------------------------------------|
| Syntax | `<element dj-attr="attribute1;value1~attribute2;value2~attribute3;value3"></element>` | | Syntax | `<element dj-attr="attribute1;value1~attribute2;value2~attribute3;value3"></element>` |
@@ -28,4 +34,61 @@
|----------------|-------------------------------------------------------------| |----------------|-------------------------------------------------------------|
| Syntax | `<element dj-for-wrap="item in list">Content</element>` | | Syntax | `<element dj-for-wrap="item in list">Content</element>` |
| Example Input | `<div dj-for-wrap="item in list">Content</div>` | | Example Input | `<div dj-for-wrap="item in list">Content</div>` |
| Example Output | `{% for item in list %}<div>Content</div>{% endfor %}` | | Example Output | `{% for item in list %}<div>Content</div>{% endfor %}` |
| dj-if, dj-elif, dj-else | Replace the elements with a Django template if-elif-else structure. |
|-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|
| Syntax | `<element dj-if="condition">Content</element>`<br/>`<element dj-elif="condition">Content</element>`<br/>`<element dj-else="condition">Content</element>` |
| Note | The `dj-elif` and `dj-else` attributes are optional. Multiple `dj-elif` attributes are allowed. |
| Example Input | `<div dj-if="condition1">Test</div><div dj-elif="condition2">Test2</div><div dj-else>Test3</div>` |
| Example Output | `{% if condition1 %}Test{% elif condition2 %}Test2{% else %}Test3{% endif %}` |
| dj-include | Replace the content of the element with a Django template include command. |
|----------------|----------------------------------------------------------------------------|
| Syntax | `<element dj-include="template_name"></element>` |
| Example Input | `<div dj-include="template_name.html"></div>` |
| Example Output | `{% include "template_name.html" %}` |
| dj-remove-default | Remove the Doctype, "head" element, and "script" element within the "body" element. It also unwraps the "body" and "html" elements, leaving only the remaining content of the "body" element. |
|-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Syntax | `<html dj-remove-default>...</html>` |
| Note | This is meant to remove all the default html wrapper code most static html generators produce. |
| Example Input | `<!DOCTYPE html><html dj-remove-default><head>...</head><body>Content<script>...</script></body></html>` |
| Example Output | `Content` |
| dj-remove | Remove the element. |
|----------------|---------------------------------|
| Syntax | `<element dj-remove></element>` |
| Example Input | `<div><p dj-remove></p></div>` |
| Example Output | ` ` |
| dj-static | Replace the value of the specified tag with a Django template static command, using the specified static file path. |
|----------------|---------------------------------------------------------------------------------------------------------------------|
| Syntax | `<element dj-static="tag;static_file_path"></element>` |
| Example Input | `<img dj-static="src;images/pic.jpg">` |
| Example Output | `<img src="{% static 'images/pic.jpg' %}"/>` |
| dj-style | Modify the style attribute of a element with django variables. |
|----------------|--------------------------------------------------------------------------------------------|
| Syntax | `<element dj-style="property1;value1;value_type1~property2;value2;value_type2"></element>` |
| Note | The `value_type` is optional. If not included still add the `;` after the value. |
| Example Input | `<div style="color:red" dj-style="background-color;color;~padding;padding;px">` |
| Example Output | `<div style="color:red;background-color:{{ color }};padding:{{ padding }}px">` |
| dj-style-raw | Modify the style attribute of a element. The value will be inserted as is. |
|----------------|----------------------------------------------------------------------------------|
| Syntax | `<element dj-style-raw="property1;value1~property2;value2"></element>` |
| Example Input | `<div style="color:red" dj-style-raw="background-color;blue~color:{{ color }}">` |
| Example Output | `<div style="color:{{ color }};background-color:blue">` |
| dj-unwrap | Remove the element's tags but keeps its content. |
|----------------|--------------------------------------------------|
| Syntax | `<element dj-unwrap>Content</element>` |
| Example Input | `<div dj-unwrap><p>Hello, world!</p></div>` |
| Example Output | `<p>Hello, world!</p>` |
| dj-var | Replace the element's content with a Django template variable. |
|----------------|----------------------------------------------------------------|
| Syntax | `<element dj-var="variable_name">Content</element>` |
| Example Input | `<p dj-var="greeting">Hello, world!</p>` |
| Example Output | `<p>{{ greeting }}</p>` |
-1
View File
@@ -1 +0,0 @@
__version__ = "0.0.1"
+4
View File
@@ -50,6 +50,8 @@ def convert_file(path: str, overwrite: bool = False) -> None:
if not (os.path.exists(path) and os.path.isfile(path)): if not (os.path.exists(path) and os.path.isfile(path)):
raise FileNotFoundError raise FileNotFoundError
print("moving the code around")
with open(path, "r") as file: with open(path, "r") as file:
soup = bs4.BeautifulSoup(file, "html.parser") soup = bs4.BeautifulSoup(file, "html.parser")
@@ -58,6 +60,8 @@ def convert_file(path: str, overwrite: bool = False) -> None:
for formatter in formatters: for formatter in formatters:
formatter.format(soup) formatter.format(soup)
print("even in between")
if overwrite: if overwrite:
out_path = path out_path = path
else: else:
+4 -4
View File
@@ -6,10 +6,10 @@ Each formatter module contains a `format` function that performs a specific modi
The `formatters` list in this package contains all the formatter modules for easy access and usage. The `formatters` list in this package contains all the formatter modules for easy access and usage.
""" """
from . import (for_formatter, if_formatter, var_formatter, block_formatter, static_formatter, attr_formatter, from . import (for_formatter, if_formatter, var_formatter, block_formatter, static_formatter, attr_formatter,
for_wrap_formatter, unwrap_formatter, include_formatter, remove_default_formatter, command_formatter, for_wrap_formatter, unwrap_formatter, remove_formatter, include_formatter, assets_formatter,
style_formatter, style_raw_formatter) remove_default_formatter, command_formatter, style_formatter, style_raw_formatter)
formatters = [for_formatter, if_formatter, var_formatter, block_formatter, static_formatter, attr_formatter, formatters = [for_formatter, if_formatter, var_formatter, block_formatter, static_formatter, attr_formatter,
for_wrap_formatter, unwrap_formatter, include_formatter, remove_default_formatter, command_formatter, for_wrap_formatter, unwrap_formatter, remove_formatter, include_formatter, assets_formatter,
style_formatter, style_raw_formatter] remove_default_formatter, command_formatter, style_formatter, style_raw_formatter]
@@ -0,0 +1,32 @@
"""
This module contains a function `format` that modifies a BeautifulSoup object in place. The function searches for
the "html" element with the attribute "dj-assets". The value of "dj-assets" is expected to be a string with two parts
separated by a semicolon (;), the first part being the asset identifier and the second part being the asset prefix.
For each element in the BeautifulSoup object with a "href" or "src" attribute that starts with the asset identifier,
it adds the asset prefix to the attribute value and wraps it in a Django static command.
After processing all elements, it removes the "dj-assets" attribute from the "html" element.
Example:
If the "html" element is <html dj-assets="assets;module">...</html>,
and there is an element <img src="assets/image.png">,
after processing, the "img" element becomes <img src="{% static 'module/assets/image.png' %}">.
"""
from bs4 import BeautifulSoup, Tag
def format(soup: BeautifulSoup) -> None:
if html := soup.find("html"):
if isinstance(html, Tag):
if html.has_attr("dj-assets"):
if isinstance(html["dj-assets"], str):
asset_identifier, asset_prefix = html["dj-assets"].split(";")
for element in soup.find_all(attrs={"href": True}):
if element["href"].startswith(asset_identifier):
element["href"] = f"{{% static '{asset_prefix}/{element['href']}' %}}"
for element in soup.find_all(attrs={"src": True}):
if element["src"].startswith(asset_identifier):
element["src"] = f"{{% static '{asset_prefix}/{element['src']}' %}}"
del html["dj-assets"]
@@ -0,0 +1,15 @@
"""
This module contains a function `format` that modifies a BeautifulSoup object in place. The function searches for
all elements with the attribute "dj-remove". For each found element, it calls the `decompose` method, which removes
the element from the parse tree. This effectively removes all elements marked with "dj-remove" from the HTML document.
Example:
If an element is <div dj-remove></div>,
after processing, it will be removed from the BeautifulSoup object.
"""
from bs4 import BeautifulSoup
def format(soup: BeautifulSoup) -> None:
for element in soup.find_all(attrs={"dj-remove": True}):
element.decompose()
+5 -4
View File
@@ -1,5 +1,5 @@
[build-system] [build-system]
requires = ["hatchling"] requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build" build-backend = "hatchling.build"
[project] [project]
@@ -11,7 +11,7 @@ authors = [
description = "HTML to Django Converter" description = "HTML to Django Converter"
readme = {file = "README.md", content-type = "text/markdown"} readme = {file = "README.md", content-type = "text/markdown"}
license = {file = "LICENSE.txt"} license = {file = "LICENSE.txt"}
keywords = ["html", "django"] keywords = ["html", "converter", "django", "tool", "web development", "static html"]
requires-python = ">= 3.12" requires-python = ">= 3.12"
classifiers = [ classifiers = [
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
@@ -29,7 +29,8 @@ test = [
"pycodestyle==2.11.1" "pycodestyle==2.11.1"
] ]
dev = [ dev = [
"hatch==1.10.0" "hatch==1.10.0",
"hatch-vcs==0.4.0"
] ]
[project.urls] [project.urls]
@@ -41,4 +42,4 @@ html_to_django = "html_to_django.converter:command_entry"
html_to_django_r = "html_to_django.converter:command_entry_r" html_to_django_r = "html_to_django.converter:command_entry_r"
[tool.hatch.version] [tool.hatch.version]
path = "html_to_django/__about__.py" source = "vcs"
@@ -0,0 +1,31 @@
from unittest import TestCase
from bs4 import BeautifulSoup
from html_to_django.formatters import assets_formatter
class AssetsTests(TestCase):
def test_basic(self) -> None:
soup = BeautifulSoup('<html dj-assets="assets;module"><img src="assets/image.png"></html>', 'html.parser')
assets_formatter.format(soup)
expected_result = '<html><img src="{% static \'module/assets/image.png\' %}"/></html>'
self.assertEqual(expected_result, str(soup))
def test_empty(self) -> None:
soup = BeautifulSoup('<html dj-assets="assets;module"></html>', 'html.parser')
assets_formatter.format(soup)
expected_result = '<html></html>'
self.assertEqual(expected_result, str(soup))
def test_no_assets(self) -> None:
soup = BeautifulSoup('<html dj-assets="assets;module"><p>Test</p></html>', 'html.parser')
assets_formatter.format(soup)
expected_result = '<html><p>Test</p></html>'
self.assertEqual(expected_result, str(soup))
def test_with_multiple_assets(self) -> None:
soup = BeautifulSoup('<html dj-assets="assets;module"><img src="assets/image1.png"><img '
'src="assets/image2.png"></html>', 'html.parser')
assets_formatter.format(soup)
expected_result = ('<html><img src="{% static \'module/assets/image1.png\' %}"/><img src="{% static '
'\'module/assets/image2.png\' %}"/></html>')
self.assertEqual(expected_result, str(soup))
@@ -0,0 +1,23 @@
from unittest import TestCase
from bs4 import BeautifulSoup
from html_to_django.formatters import remove_formatter
class RemoveTests(TestCase):
def test_basic(self) -> None:
soup = BeautifulSoup('<div dj-remove><div>Test</div></div>', "html.parser")
remove_formatter.format(soup)
expected_result = ""
self.assertEqual(expected_result, str(soup))
def test_empty(self) -> None:
soup = BeautifulSoup('<div dj-remove></div>', "html.parser")
remove_formatter.format(soup)
expected_result = ""
self.assertEqual(expected_result, str(soup))
def test_with_remove_inside(self) -> None:
soup = BeautifulSoup('<div dj-remove><div dj-remove>Inner Test</div></div>', "html.parser")
remove_formatter.format(soup)
expected_result = ""
self.assertEqual(expected_result, str(soup))
+3 -3
View File
@@ -5,19 +5,19 @@ from html_to_django.formatters import unwrap_formatter
class UnwrapTests(TestCase): class UnwrapTests(TestCase):
def test_basic(self) -> None: def test_basic(self) -> None:
soup = BeautifulSoup('<div dj-unwrap="true"><div>Test</div></div>', "html.parser") soup = BeautifulSoup('<div dj-unwrap><div>Test</div></div>', "html.parser")
unwrap_formatter.format(soup) unwrap_formatter.format(soup)
expected_result = "<div>Test</div>" expected_result = "<div>Test</div>"
self.assertEqual(expected_result, str(soup)) self.assertEqual(expected_result, str(soup))
def test_empty(self) -> None: def test_empty(self) -> None:
soup = BeautifulSoup('<div dj-unwrap="true"></div>', "html.parser") soup = BeautifulSoup('<div dj-unwrap></div>', "html.parser")
unwrap_formatter.format(soup) unwrap_formatter.format(soup)
expected_result = "" expected_result = ""
self.assertEqual(expected_result, str(soup)) self.assertEqual(expected_result, str(soup))
def test_with_unwrap_inside(self) -> None: def test_with_unwrap_inside(self) -> None:
soup = BeautifulSoup('<div dj-unwrap="true"><div dj-unwrap="true">Inner Test</div></div>', "html.parser") soup = BeautifulSoup('<div dj-unwrap><div dj-unwrap>Inner Test</div></div>', "html.parser")
unwrap_formatter.format(soup) unwrap_formatter.format(soup)
expected_result = "Inner Test" expected_result = "Inner Test"
self.assertEqual(expected_result, str(soup)) self.assertEqual(expected_result, str(soup))
+25 -60
View File
@@ -7,9 +7,24 @@ from io import StringIO
from unittest import TestCase from unittest import TestCase
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from glob import glob from glob import glob
from typing import Any
from html_to_django.converter import get_required_libraries, convert_file, convert_dir, convert_path from html_to_django.converter import get_required_libraries, convert_file, convert_dir, convert_path
class TestDirectory(tempfile.TemporaryDirectory): # type: ignore
"""
Context Manager that creates a temporary directory containing the test project.
"""
def __enter__(self) -> Any:
tmpdir = super().__enter__()
shutil.copytree(
"tests/test_project",
tmpdir,
dirs_exist_ok=True
)
return tmpdir
class ConverterTests(TestCase): class ConverterTests(TestCase):
def test_get_required_libraries(self) -> None: def test_get_required_libraries(self) -> None:
html = '<div dj-libs="lib1;lib2">Content</div><div dj-libs="lib3"></div>' html = '<div dj-libs="lib1;lib2">Content</div><div dj-libs="lib3"></div>'
@@ -24,12 +39,7 @@ class ConverterTests(TestCase):
self.assertEqual(expected, get_required_libraries(soup)) self.assertEqual(expected, get_required_libraries(soup))
def test_convert_file(self) -> None: def test_convert_file(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir: with TestDirectory() as tmpdir:
shutil.copytree(
"tests/test_project",
tmpdir,
dirs_exist_ok=True
)
convert_file(f"{tmpdir}/test.html") convert_file(f"{tmpdir}/test.html")
# verify that the new file exists and has the expected value # verify that the new file exists and has the expected value
self.assertTrue(filecmp.cmp( self.assertTrue(filecmp.cmp(
@@ -43,12 +53,7 @@ class ConverterTests(TestCase):
)) ))
def test_convert_file_overwrite(self) -> None: def test_convert_file_overwrite(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir: with TestDirectory() as tmpdir:
shutil.copytree(
"tests/test_project",
tmpdir,
dirs_exist_ok=True
)
convert_file(f"{tmpdir}/test.html", True) convert_file(f"{tmpdir}/test.html", True)
# verify that the source file has been overwriten # verify that the source file has been overwriten
self.assertTrue(filecmp.cmp( self.assertTrue(filecmp.cmp(
@@ -59,12 +64,7 @@ class ConverterTests(TestCase):
self.assertFalse(os.path.exists(f"{tmpdir}/test.n.html")) self.assertFalse(os.path.exists(f"{tmpdir}/test.n.html"))
def test_convert_file_non_html_extension(self) -> None: def test_convert_file_non_html_extension(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir: with TestDirectory() as tmpdir:
shutil.copytree(
"tests/test_project",
tmpdir,
dirs_exist_ok=True
)
os.rename(f"{tmpdir}/test.html", f"{tmpdir}/test.txt") os.rename(f"{tmpdir}/test.html", f"{tmpdir}/test.txt")
convert_file(f"{tmpdir}/test.txt") convert_file(f"{tmpdir}/test.txt")
@@ -75,12 +75,7 @@ class ConverterTests(TestCase):
)) ))
def test_convert_file_no_extension(self) -> None: def test_convert_file_no_extension(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir: with TestDirectory() as tmpdir:
shutil.copytree(
"tests/test_project",
tmpdir,
dirs_exist_ok=True
)
os.rename(f"{tmpdir}/test.html", f"{tmpdir}/test") os.rename(f"{tmpdir}/test.html", f"{tmpdir}/test")
convert_file(f"{tmpdir}/test") convert_file(f"{tmpdir}/test")
@@ -99,12 +94,7 @@ class ConverterTests(TestCase):
convert_file("tests/test_project") convert_file("tests/test_project")
def test_convert_dir(self) -> None: def test_convert_dir(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir: with TestDirectory() as tmpdir:
shutil.copytree(
"tests/test_project",
tmpdir,
dirs_exist_ok=True
)
convert_dir(f"{tmpdir}") convert_dir(f"{tmpdir}")
for conversion_result in glob(f"{tmpdir}/**/*.n.html", recursive=True): for conversion_result in glob(f"{tmpdir}/**/*.n.html", recursive=True):
@@ -125,12 +115,7 @@ class ConverterTests(TestCase):
)) ))
def test_convert_dir_overwrite(self) -> None: def test_convert_dir_overwrite(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir: with TestDirectory() as tmpdir:
shutil.copytree(
"tests/test_project",
tmpdir,
dirs_exist_ok=True
)
convert_dir(f"{tmpdir}", True) convert_dir(f"{tmpdir}", True)
for conversion_result in glob(f"{tmpdir}/**/*.html", recursive=True): for conversion_result in glob(f"{tmpdir}/**/*.html", recursive=True):
@@ -153,12 +138,7 @@ class ConverterTests(TestCase):
convert_dir("tests/test_project/test.html") convert_dir("tests/test_project/test.html")
def test_convert_path_file(self) -> None: def test_convert_path_file(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir: with TestDirectory() as tmpdir:
shutil.copytree(
"tests/test_project",
tmpdir,
dirs_exist_ok=True
)
convert_path(f"{tmpdir}/test.html") convert_path(f"{tmpdir}/test.html")
# verify that the new file exists and has the expected value # verify that the new file exists and has the expected value
self.assertTrue(filecmp.cmp( self.assertTrue(filecmp.cmp(
@@ -172,12 +152,7 @@ class ConverterTests(TestCase):
)) ))
def test_convert_path_file_overwrite(self) -> None: def test_convert_path_file_overwrite(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir: with TestDirectory() as tmpdir:
shutil.copytree(
"tests/test_project",
tmpdir,
dirs_exist_ok=True
)
convert_path(f"{tmpdir}/test.html", True) convert_path(f"{tmpdir}/test.html", True)
# verify that the source file has been overwriten # verify that the source file has been overwriten
self.assertTrue(filecmp.cmp( self.assertTrue(filecmp.cmp(
@@ -188,12 +163,7 @@ class ConverterTests(TestCase):
self.assertFalse(os.path.exists(f"{tmpdir}/test.n.html")) self.assertFalse(os.path.exists(f"{tmpdir}/test.n.html"))
def test_convert_path_dir(self) -> None: def test_convert_path_dir(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir: with TestDirectory() as tmpdir:
shutil.copytree(
"tests/test_project",
tmpdir,
dirs_exist_ok=True
)
convert_path(f"{tmpdir}") convert_path(f"{tmpdir}")
for conversion_result in glob(f"{tmpdir}/**/*.n.html", recursive=True): for conversion_result in glob(f"{tmpdir}/**/*.n.html", recursive=True):
@@ -214,12 +184,7 @@ class ConverterTests(TestCase):
)) ))
def test_convert_path_dir_overwrite(self) -> None: def test_convert_path_dir_overwrite(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir: with TestDirectory() as tmpdir:
shutil.copytree(
"tests/test_project",
tmpdir,
dirs_exist_ok=True
)
convert_path(f"{tmpdir}", True) convert_path(f"{tmpdir}", True)
for conversion_result in glob(f"{tmpdir}/**/*.html", recursive=True): for conversion_result in glob(f"{tmpdir}/**/*.html", recursive=True):