diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f098822..0d46532 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,10 +14,11 @@ permissions: jobs: test: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} strategy: matrix: + os: [ubuntu-latest, windows-latest] python-version: [ "3.11", "3.12", "3.13" ] steps: diff --git a/pyproject.toml b/pyproject.toml index 8a57edd..19d2509 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,7 @@ requires-python = ">=3.10" dependencies = [ "inquirerpy>=0.3.4", "requests>=2.32.3", + "retry>=0.9.2", "setuptools>=75.1.0", ] diff --git a/src/dj_beat_drop/new.py b/src/dj_beat_drop/new.py index df0872c..72c2f3a 100644 --- a/src/dj_beat_drop/new.py +++ b/src/dj_beat_drop/new.py @@ -6,7 +6,7 @@ from InquirerPy import inquirer from dj_beat_drop import utils -from dj_beat_drop.utils import color +from dj_beat_drop.utils import color, remove_directory def rename_template_files(project_dir): @@ -92,7 +92,7 @@ def create_new_project( os.chdir(project_dir) os.system("uv init") # noqa: S605, S607 os.system("rm hello.py") # noqa: S605, S607 - os.system(f"uv add django~='{minor_version}'") # noqa: S605 + os.system(f'uv add "django~={minor_version}"') # noqa: S605 if initialize_env is True: os.system("uv add environs[django]") # noqa: S605, S607 os.system("uv run manage.py migrate") # noqa: S605, S607 @@ -125,7 +125,7 @@ def handle_new(name: str, use_lts: bool, overwrite_target_dir: bool) -> None: if overwrite_response is False: color.red("Operation cancelled.") return - shutil.rmtree(project_dir) + remove_directory(project_dir) initialize_uv = inquirer.confirm(message="Initialize your project with UV?", default=True).execute() initialize_env = inquirer.confirm( diff --git a/src/dj_beat_drop/utils.py b/src/dj_beat_drop/utils.py index 1a36ad6..260ab6c 100644 --- a/src/dj_beat_drop/utils.py +++ b/src/dj_beat_drop/utils.py @@ -1,7 +1,9 @@ import secrets +import shutil from functools import lru_cache import requests +from retry import retry class Color: @@ -66,3 +68,9 @@ def get_secret_key(): """Return a 50 character random string usable as a SECRET_KEY setting value.""" chars = "abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)" return "".join(secrets.choice(chars) for _ in range(50)) + + +@retry(tries=5, delay=2, backoff=2, exceptions=(PermissionError,)) +def remove_directory(path): + """Use retry to handle PermissionError when trying to remove a directory on Windows.""" + shutil.rmtree(path) diff --git a/tests/test_new_command.py b/tests/test_new_command.py index f089522..036b36b 100644 --- a/tests/test_new_command.py +++ b/tests/test_new_command.py @@ -1,11 +1,12 @@ +import platform import random import re -import shutil import string from pathlib import Path from unittest import TestCase from dj_beat_drop.new import create_new_project +from dj_beat_drop.utils import remove_directory ENV_SECRET_KEY_PATTERN = 'SECRET_KEY = env.str("SECRET_KEY")' # noqa: S105 FILE_ASSERTIONS = { @@ -82,11 +83,11 @@ def setUp(self): "initialize_env": True, } if self.project_dir.exists(): - shutil.rmtree(self.project_dir) + remove_directory(self.project_dir) def tearDown(self): - if self.project_dir.exists(): - shutil.rmtree(self.project_dir) + if self.project_dir.exists() and platform.system() != "Windows": + remove_directory(self.project_dir) @staticmethod def assert_files_are_correct( diff --git a/uv.lock b/uv.lock index 58c9fd0..29e2846 100644 --- a/uv.lock +++ b/uv.lock @@ -111,6 +111,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] +[[package]] +name = "decorator" +version = "5.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/0c/8d907af351aa16b42caae42f9d6aa37b900c67308052d10fdce809f8d952/decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330", size = 35016 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/50/83c593b07763e1161326b3b8c6686f0f4b0f24d5526546bee538c89837d6/decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186", size = 9073 }, +] + [[package]] name = "dj-beat-drop" version = "0.4.0" @@ -118,6 +127,7 @@ source = { editable = "." } dependencies = [ { name = "inquirerpy" }, { name = "requests" }, + { name = "retry" }, { name = "setuptools" }, ] @@ -132,6 +142,7 @@ dev = [ requires-dist = [ { name = "inquirerpy", specifier = ">=0.3.4" }, { name = "requests", specifier = ">=2.32.3" }, + { name = "retry", specifier = ">=0.9.2" }, { name = "setuptools", specifier = ">=75.1.0" }, ] @@ -234,6 +245,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a9/6a/fd08d94654f7e67c52ca30523a178b3f8ccc4237fce4be90d39c938a831a/prompt_toolkit-3.0.48-py3-none-any.whl", hash = "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e", size = 386595 }, ] +[[package]] +name = "py" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/ff/fec109ceb715d2a6b4c4a85a61af3b40c723a961e8828319fbcb15b868dc/py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", size = 207796 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378", size = 98708 }, +] + [[package]] name = "pygls" version = "1.3.1" @@ -279,6 +299,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, ] +[[package]] +name = "retry" +version = "0.9.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "decorator" }, + { name = "py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/72/75d0b85443fbc8d9f38d08d2b1b67cc184ce35280e4a3813cda2f445f3a4/retry-0.9.2.tar.gz", hash = "sha256:f8bfa8b99b69c4506d6f5bd3b0aabf77f98cdb17f3c9fc3f5ca820033336fba4", size = 6448 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/0d/53aea75710af4528a25ed6837d71d117602b01946b307a3912cb3cfcbcba/retry-0.9.2-py2.py3-none-any.whl", hash = "sha256:ccddf89761fa2c726ab29391837d4327f819ea14d244c232a1d24c67a2f98606", size = 7986 }, +] + [[package]] name = "ruff" version = "0.6.9"