From 8eb74e6c68f8425b66f092f1c1e516260dcf2d85 Mon Sep 17 00:00:00 2001
From: Filip Lajszczak <filip@lajszczak.dev>
Date: Tue, 26 Nov 2024 14:37:22 +0000
Subject: [PATCH] Updates for different python and django versions.

Version bump and new core.
Actions betterification.

Co-authored-by: Piotr Kaznowski <piotr@kazno.dev>
---
 .github/workflows/tests.yaml                  |   9 +-
 cli/django.py                                 |   7 +
 pythonanywhere/__init__.py                    |   2 +-
 setup.py                                      |   6 +-
 tests/conftest.py                             |  19 +++
 tests/test_cli_django.py                      |  20 +--
 tests/test_django_project.py                  |  22 ++--
 tests/test_pa_autoconfigure_django.py         |  13 +-
 ..._pa_start_django_webapp_with_virtualenv.py | 124 +++++++++---------
 tests/test_virtualenvs.py                     |   6 +-
 10 files changed, 126 insertions(+), 102 deletions(-)

diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index 98d3d2c..fa9ed53 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -1,24 +1,19 @@
 name: Tests
 
-on: [push]
+on: [push, pull_request]
 
 jobs:
   build:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        python-version: [ "3.8", "3.9", "3.10", "3.11" ]
+        python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12", "3.13" ]
 
     name: Python ${{ matrix.python-version }}
     steps:
 
     - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
 
-    - name: Setup timezone
-      uses: zcong1993/setup-timezone@master
-      with:
-        timezone: UTC
-
     - name: Set up Python
       uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5
       with:
diff --git a/cli/django.py b/cli/django.py
index 7c6cc0a..ac36b7b 100644
--- a/cli/django.py
+++ b/cli/django.py
@@ -12,6 +12,12 @@
 @app.command()
 def autoconfigure(
     repo_url: str = typer.Argument(..., help="url of remote git repository of your django project"),
+    branch: str = typer.Option(
+        "None",
+        "-b",
+        "--branch",
+        help="Branch name in case of multiple branches",
+    ),
     domain_name: str = typer.Option(
         "your-username.pythonanywhere.com",
         "-d",
@@ -43,6 +49,7 @@ def autoconfigure(
     project = DjangoProject(domain, python_version)
     project.sanity_checks(nuke=nuke)
     project.download_repo(repo_url, nuke=nuke),
+    project.ensure_branch(branch),
     project.create_virtualenv(nuke=nuke)
     project.create_webapp(nuke=nuke)
     project.add_static_file_mappings()
diff --git a/pythonanywhere/__init__.py b/pythonanywhere/__init__.py
index 8782a8b..32e2f39 100644
--- a/pythonanywhere/__init__.py
+++ b/pythonanywhere/__init__.py
@@ -1 +1 @@
-__version__ = "0.15.4"
+__version__ = "0.15.5"
diff --git a/setup.py b/setup.py
index 09c26a6..ef57b92 100644
--- a/setup.py
+++ b/setup.py
@@ -9,7 +9,7 @@
 
 setup(
     name="pythonanywhere",
-    version="0.15.4",
+    version="0.15.5",
     description="PythonAnywhere helper tools for users",
     long_description=long_description,
     long_description_content_type="text/markdown",
@@ -22,6 +22,8 @@
         "Intended Audience :: Developers",
         "Topic :: Software Development :: Libraries",
         "License :: OSI Approved :: MIT License",
+        "Programming Language :: Python :: 3.13",
+        "Programming Language :: Python :: 3.12",
         "Programming Language :: Python :: 3.11",
         "Programming Language :: Python :: 3.10",
         "Programming Language :: Python :: 3.9",
@@ -36,7 +38,7 @@
         "docopt",
         "packaging",
         "python-dateutil",
-        "pythonanywhere_core==0.2.3",
+        "pythonanywhere_core==0.2.4",
         "requests",
         "schema",
         "snakesay",
diff --git a/tests/conftest.py b/tests/conftest.py
index ecd66b4..37794ef 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -3,6 +3,7 @@
 import tempfile
 from getpass import getuser
 from pathlib import Path
+from platform import python_version
 from unittest.mock import Mock, patch
 
 import psutil
@@ -120,3 +121,21 @@ def process_killer():
         for child in psutil.Process(p.pid).children():
             child.kill()
         p.kill()
+
+@pytest.fixture
+def running_python_version():
+    return ".".join(python_version().split(".")[:2])
+
+@pytest.fixture
+def new_django_version(running_python_version):
+    if running_python_version in ["3.10", "3.11", "3.12", "3.13"]:
+        return "5.1.3"
+    else:
+        return "4.2.16"
+
+@pytest.fixture
+def old_django_version(running_python_version):
+    if running_python_version in ["3.10", "3.11", "3.12", "3.13"]:
+        return "5.1.2"
+    else:
+        return "4.2.15"
diff --git a/tests/test_cli_django.py b/tests/test_cli_django.py
index 726dbde..9d7a4a3 100644
--- a/tests/test_cli_django.py
+++ b/tests/test_cli_django.py
@@ -59,6 +59,7 @@ def test_autoconfigure_calls_all_stuff_in_right_order(mock_django_project):
     assert mock_django_project.return_value.method_calls == [
         call.sanity_checks(nuke=True),
         call.download_repo("repo.url", nuke=True),
+        call.ensure_branch("None"),
         call.create_virtualenv(nuke=True),
         call.create_webapp(nuke=True),
         call.add_static_file_mappings(),
@@ -86,6 +87,8 @@ def test_autoconfigure_actually_works_against_example_repo(
     process_killer,
     running_python_version,
 ):
+    git_ref = "non-nested-old" if running_python_version in ["3.8", "3.9"] else "master"
+    expected_django_version = "4.2.16" if running_python_version in ["3.8", "3.9"] else "5.1.3"
     mocker.patch("cli.django.DjangoProject.start_bash")
     repo = "https://github.com/pythonanywhere/example-django-project.git"
     domain = "mydomain.com"
@@ -99,10 +102,11 @@ def test_autoconfigure_actually_works_against_example_repo(
             domain,
             "-p",
             running_python_version,
+            "--branch",
+            git_ref,
         ],
     )
 
-    expected_django_version = "3.0.6"
     expected_virtualenv = virtualenvs_folder / domain
     expected_project_path = fake_home / domain
     django_project_name = "myproject"
@@ -122,7 +126,7 @@ def test_autoconfigure_actually_works_against_example_repo(
 
     with expected_settings_path.open() as f:
         lines = f.read().split("\n")
-    assert "MEDIA_ROOT = os.path.join(BASE_DIR, 'media')" in lines
+    assert "MEDIA_ROOT = Path(BASE_DIR / 'media')" in lines
     assert "ALLOWED_HOSTS = ['mydomain.com']  # type: List[str]" in lines
 
     assert "base.css" in os.listdir(str(fake_home / domain / "static/admin/css"))
@@ -180,6 +184,7 @@ def test_start_actually_creates_django_project_in_virtualenv_with_hacked_setting
     virtualenvs_folder,
     api_token,
     running_python_version,
+    new_django_version,
 ):
     runner.invoke(
         app,
@@ -188,7 +193,7 @@ def test_start_actually_creates_django_project_in_virtualenv_with_hacked_setting
             "-d",
             "mydomain.com",
             "-j",
-            "2.2.12",
+            new_django_version,
             "-p",
             running_python_version,
         ],
@@ -204,11 +209,11 @@ def test_start_actually_creates_django_project_in_virtualenv_with_hacked_setting
         .decode()
         .strip()
     )
-    assert django_version == "2.2.12"
+    assert django_version == new_django_version
 
     with (fake_home / "mydomain.com/mysite/settings.py").open() as f:
         lines = f.read().split("\n")
-    assert "MEDIA_ROOT = os.path.join(BASE_DIR, 'media')" in lines
+    assert "MEDIA_ROOT = Path(BASE_DIR / 'media')" in lines
     assert "ALLOWED_HOSTS = ['mydomain.com']" in lines
 
     assert "base.css" in os.listdir(str(fake_home / "mydomain.com/static/admin/css"))
@@ -222,10 +227,9 @@ def test_nuke_option_lets_you_run_twice(
     virtualenvs_folder,
     api_token,
     running_python_version,
+    old_django_version,
+    new_django_version,
 ):
-    old_django_version = "2.2.12"
-    new_django_version = "3.0.6"
-
     runner.invoke(
         app,
         [
diff --git a/tests/test_django_project.py b/tests/test_django_project.py
index 10507ff..d2c7469 100644
--- a/tests/test_django_project.py
+++ b/tests/test_django_project.py
@@ -247,19 +247,21 @@ def test_nuke_option_handles_directory_not_existing(self, mock_subprocess, fake_
 
 
 @pytest.fixture
-def non_nested_submodule():
+def non_nested_submodule(running_python_version):
+    git_ref = "non-nested-old" if running_python_version in ["3.8", "3.9"] else "master"
     subprocess.check_call(["git", "submodule", "update", "--init", "--recursive"])
     submodule_path = Path(__file__).parents[1] / "submodules" / "example-django-project"
-    subprocess.check_call(["git", "checkout", "master"], cwd=str(submodule_path))
+    subprocess.check_call(["git", "checkout", git_ref], cwd=str(submodule_path))
     yield submodule_path
     subprocess.check_call(["git", "submodule", "update", "--init", "--recursive"])
 
 
 @pytest.fixture
-def more_nested_submodule():
+def more_nested_submodule(running_python_version):
+    git_ref = "more-nested-old" if running_python_version in ["3.8", "3.9"] else "morenested"
     subprocess.check_call(["git", "submodule", "update", "--init", "--recursive"])
     submodule_path = Path(__file__).parents[1] / "submodules" / "example-django-project"
-    subprocess.check_call(["git", "checkout", "morenested"], cwd=str(submodule_path))
+    subprocess.check_call(["git", "checkout", git_ref], cwd=str(submodule_path))
     yield submodule_path
     subprocess.check_call(["git", "submodule", "update", "--init", "--recursive"])
 
@@ -509,9 +511,8 @@ def test_updates_wsgi_file_from_template(self, virtualenvs_folder):
 
     @pytest.mark.slowtest
     def test_actually_produces_wsgi_file_that_can_import_project_non_nested(
-        self, fake_home, non_nested_submodule, virtualenvs_folder
+        self, fake_home, non_nested_submodule, virtualenvs_folder, running_python_version
     ):
-        running_python_version = ".".join(python_version().split(".")[:2])
         project = DjangoProject("mydomain.com", running_python_version)
         shutil.copytree(str(non_nested_submodule), str(project.project_path))
         if running_python_version in ["3.8", "3.9", "3.10", "3.11"]:
@@ -529,15 +530,14 @@ def test_actually_produces_wsgi_file_that_can_import_project_non_nested(
 
     @pytest.mark.slowtest
     def test_actually_produces_wsgi_file_that_can_import_nested_project(
-        self, fake_home, more_nested_submodule, virtualenvs_folder
+        self, fake_home, more_nested_submodule, virtualenvs_folder, running_python_version
     ):
-        running_python_version = ".".join(python_version().split(".")[:2])
         project = DjangoProject("mydomain.com", running_python_version)
         shutil.copytree(str(more_nested_submodule), str(project.project_path))
-        if running_python_version in ["3.8", "3.9", "3.10", "3.11"]:
-            project.create_virtualenv(django_version="latest")
-        else:
+        if running_python_version in ["3.8", "3.9"]:
             project.create_virtualenv()
+        else:
+            project.create_virtualenv(django_version="latest")
 
         project.find_django_files()
         project.wsgi_file_path = Path(tempfile.NamedTemporaryFile().name)
diff --git a/tests/test_pa_autoconfigure_django.py b/tests/test_pa_autoconfigure_django.py
index fbf879f..1e4e90c 100644
--- a/tests/test_pa_autoconfigure_django.py
+++ b/tests/test_pa_autoconfigure_django.py
@@ -1,4 +1,3 @@
-from platform import python_version
 from unittest.mock import call, patch
 import os
 import pytest
@@ -7,6 +6,7 @@
 import time
 
 from scripts.pa_autoconfigure_django import main
+from tests.conftest import new_django_version
 
 
 class TestMain:
@@ -33,9 +33,9 @@ def test_calls_all_stuff_in_right_order(self):
 
     @pytest.mark.slowtest
     def test_actually_works_against_example_repo(
-        self, fake_home, virtualenvs_folder, api_token, process_killer
+        self, fake_home, virtualenvs_folder, api_token, process_killer, running_python_version, new_django_version
     ):
-        running_python_version = ".".join(python_version().split(".")[:2])
+        git_ref = "non-nested-old" if running_python_version in ["3.8", "3.9"] else "master"
         repo = 'https://github.com/pythonanywhere/example-django-project.git'
         domain = 'mydomain.com'
         with patch('scripts.pa_autoconfigure_django.DjangoProject.update_wsgi_file'):
@@ -43,13 +43,12 @@ def test_actually_works_against_example_repo(
                 with patch('pythonanywhere_core.webapp.call_api'):
                     main(
                         repo_url=repo,
-                        branch="master",
+                        branch=git_ref,
                         domain=domain,
                         python_version=running_python_version,
                         nuke=False
                     )
 
-        expected_django_version = '3.0.6'
         expected_virtualenv = virtualenvs_folder / domain
         expected_project_path = fake_home / domain
         django_project_name = 'myproject'
@@ -60,11 +59,11 @@ def test_actually_works_against_example_repo(
             '-c'
             'import django; print(django.get_version())'
         ]).decode().strip()
-        assert django_version == expected_django_version
+        assert django_version == new_django_version
 
         with expected_settings_path.open() as f:
             lines = f.read().split('\n')
-        assert "MEDIA_ROOT = os.path.join(BASE_DIR, 'media')" in lines
+        assert "MEDIA_ROOT = Path(BASE_DIR / 'media')" in lines
         assert "ALLOWED_HOSTS = ['mydomain.com']  # type: List[str]" in lines
 
         assert 'base.css' in os.listdir(str(fake_home / domain / 'static/admin/css'))
diff --git a/tests/test_pa_start_django_webapp_with_virtualenv.py b/tests/test_pa_start_django_webapp_with_virtualenv.py
index 194ca76..c56c5d1 100644
--- a/tests/test_pa_start_django_webapp_with_virtualenv.py
+++ b/tests/test_pa_start_django_webapp_with_virtualenv.py
@@ -1,78 +1,78 @@
 import os
 import subprocess
-from platform import python_version
-from unittest.mock import call, patch
+from unittest.mock import call, patch, sentinel
 
 import pytest
 from scripts.pa_start_django_webapp_with_virtualenv import main
 
 
-class TestMain:
-    def test_calls_all_stuff_in_right_order(self):
-        with patch("scripts.pa_start_django_webapp_with_virtualenv.DjangoProject") as mock_DjangoProject:
-            main("www.domain.com", "django.version", "python.version", nuke="nuke option")
-        assert mock_DjangoProject.call_args == call("www.domain.com", "python.version")
-        assert mock_DjangoProject.return_value.method_calls == [
-            call.sanity_checks(nuke="nuke option"),
-            call.create_virtualenv("django.version", nuke="nuke option"),
-            call.run_startproject(nuke="nuke option"),
-            call.find_django_files(),
-            call.update_settings_file(),
-            call.run_collectstatic(),
-            call.create_webapp(nuke="nuke option"),
-            call.add_static_file_mappings(),
-            call.update_wsgi_file(),
-            call.webapp.reload(),
-        ]
+def test_calls_all_stuff_in_right_order(mocker):
+    mock_DjangoProject = mocker.patch(
+        "scripts.pa_start_django_webapp_with_virtualenv.DjangoProject"
+    )
 
-    @pytest.mark.slowtest
-    def test_actually_creates_django_project_in_virtualenv_with_hacked_settings_and_static_files(
-        self, fake_home, virtualenvs_folder, api_token
-    ):
-        running_python_version = ".".join(python_version().split(".")[:2])
-        with patch("scripts.pa_start_django_webapp_with_virtualenv.DjangoProject.update_wsgi_file"):
-            with patch("pythonanywhere_core.webapp.call_api"):
-                main("mydomain.com", "2.2.12", running_python_version, nuke=False)
+    main(
+        sentinel.domain, sentinel.django_version, sentinel.python_version, nuke=sentinel.nuke
+    )
+    assert mock_DjangoProject.call_args == call(sentinel.domain, sentinel.python_version)
+    assert mock_DjangoProject.return_value.method_calls == [
+        call.sanity_checks(nuke=sentinel.nuke),
+        call.create_virtualenv(sentinel.django_version, nuke=sentinel.nuke),
+        call.run_startproject(nuke=sentinel.nuke),
+        call.find_django_files(),
+        call.update_settings_file(),
+        call.run_collectstatic(),
+        call.create_webapp(nuke=sentinel.nuke),
+        call.add_static_file_mappings(),
+        call.update_wsgi_file(),
+        call.webapp.reload(),
+    ]
 
-        django_version = (
-            subprocess.check_output(
-                [
-                    str(virtualenvs_folder / "mydomain.com/bin/python"),
-                    "-c" "import django; print(django.get_version())",
-                ]
-            )
-            .decode()
-            .strip()
-        )
-        assert django_version == "2.2.12"
+@pytest.mark.slowtest
+def test_actually_creates_django_project_in_virtualenv_with_hacked_settings_and_static_files(
+    fake_home, virtualenvs_folder, api_token, running_python_version, new_django_version
+):
+    with patch("scripts.pa_start_django_webapp_with_virtualenv.DjangoProject.update_wsgi_file"):
+        with patch("pythonanywhere_core.webapp.call_api"):
+            main("mydomain.com", new_django_version, running_python_version, nuke=False)
 
-        with (fake_home / "mydomain.com/mysite/settings.py").open() as f:
-            lines = f.read().split("\n")
-        assert "MEDIA_ROOT = os.path.join(BASE_DIR, 'media')" in lines
-        assert "ALLOWED_HOSTS = ['mydomain.com']" in lines
+    output_django_version = (
+        subprocess.check_output(
+            [
+                str(virtualenvs_folder / "mydomain.com/bin/python"),
+                "-c" "import django; print(django.get_version())",
+            ]
+        )
+        .decode()
+        .strip()
+    )
+    assert output_django_version == new_django_version
 
-        assert "base.css" in os.listdir(str(fake_home / "mydomain.com/static/admin/css"))
+    with (fake_home / "mydomain.com/mysite/settings.py").open() as f:
+        lines = f.read().split("\n")
+    assert "MEDIA_ROOT = Path(BASE_DIR / 'media')" in lines
+    assert "ALLOWED_HOSTS = ['mydomain.com']" in lines
 
-    @pytest.mark.slowtest
-    def test_nuke_option_lets_you_run_twice(self, fake_home, virtualenvs_folder, api_token):
+    assert "base.css" in os.listdir(str(fake_home / "mydomain.com/static/admin/css"))
 
-        with patch("scripts.pa_start_django_webapp_with_virtualenv.DjangoProject.update_wsgi_file"):
-            with patch("pythonanywhere_core.webapp.call_api"):
-                running_python_version = ".".join(python_version().split(".")[:2])
-                old_django_version = "2.2.12"
-                new_django_version = "3.0.6"
+@pytest.mark.slowtest
+def test_nuke_option_lets_you_run_twice(
+        fake_home, virtualenvs_folder, api_token, running_python_version, new_django_version, old_django_version
+):
 
-                main("mydomain.com", old_django_version, running_python_version, nuke=False)
-                main("mydomain.com", new_django_version, running_python_version, nuke=True)
+    with patch("scripts.pa_start_django_webapp_with_virtualenv.DjangoProject.update_wsgi_file"):
+        with patch("pythonanywhere_core.webapp.call_api"):
+            main("mydomain.com", old_django_version, running_python_version, nuke=False)
+            main("mydomain.com", new_django_version, running_python_version, nuke=True)
 
-        django_version = (
-            subprocess.check_output(
-                [
-                    str(virtualenvs_folder / "mydomain.com/bin/python"),
-                    "-c" "import django; print(django.get_version())",
-                ]
-            )
-            .decode()
-            .strip()
+    django_version = (
+        subprocess.check_output(
+            [
+                str(virtualenvs_folder / "mydomain.com/bin/python"),
+                "-c" "import django; print(django.get_version())",
+            ]
         )
-        assert django_version == new_django_version
+        .decode()
+        .strip()
+    )
+    assert django_version == new_django_version
diff --git a/tests/test_virtualenvs.py b/tests/test_virtualenvs.py
index 783afad..5ce69a6 100644
--- a/tests/test_virtualenvs.py
+++ b/tests/test_virtualenvs.py
@@ -45,8 +45,7 @@ def test_install_pip_installs_each_package(self, mock_subprocess, virtualenvs_fo
         assert command_list == [pip_path, "install", "package1", "package2==1.1.2"]
 
     @pytest.mark.slowtest
-    def test_actually_installing_a_real_package(self, fake_home, virtualenvs_folder):
-        running_python_version = ".".join(python_version().split(".")[:2])
+    def test_actually_installing_a_real_package(self, fake_home, virtualenvs_folder, running_python_version):
         v = Virtualenv("www.adomain.com", running_python_version)
         v.create(nuke=False)
         v.pip_install("aafigure")
@@ -54,8 +53,7 @@ def test_actually_installing_a_real_package(self, fake_home, virtualenvs_folder)
         subprocess.check_call([str(v.path / "bin/python"), "-c" "import aafigure"])
 
     @pytest.mark.slowtest
-    def test_gets_version(self, fake_home, virtualenvs_folder):
-        running_python_version = ".".join(python_version().split(".")[:2])
+    def test_gets_version(self, fake_home, virtualenvs_folder, running_python_version):
         v = Virtualenv("www.adomain.com", running_python_version)
         v.create(nuke=False)
         v.pip_install("aafigure==0.6")