From b5d48b043119612805c8ba07616c4efaf768c90c Mon Sep 17 00:00:00 2001
From: David Douard <david.douard@sdfa3.org>
Date: Mon, 27 Nov 2023 18:08:21 +0100
Subject: [PATCH] Migrate to copier-based swh-py-template

---
 .copier-answers.yml        | 11 +++++++
 .gitignore                 | 19 ++++--------
 .pre-commit-config.yaml    | 25 ++++++++-------
 MANIFEST.in                | 16 ----------
 Makefile                   |  2 +-
 docs/Makefile              |  3 +-
 mypy.ini                   | 11 ++++---
 pyproject.toml             | 42 ++++++++++++++++++++++++++
 pytest.ini                 |  9 ++++--
 setup.py                   | 62 ++------------------------------------
 swh/__init__.py            |  3 --
 swh/web/settings/common.py | 20 ++++++++++--
 swh/web/utils/exc.py       |  1 -
 tox.ini                    | 62 +++++++++++++++++---------------------
 14 files changed, 134 insertions(+), 152 deletions(-)
 create mode 100644 .copier-answers.yml
 delete mode 100644 MANIFEST.in
 delete mode 100644 swh/__init__.py

diff --git a/.copier-answers.yml b/.copier-answers.yml
new file mode 100644
index 000000000..517130ca0
--- /dev/null
+++ b/.copier-answers.yml
@@ -0,0 +1,11 @@
+# Changes here will be overwritten by Copier
+_commit: v0.1.5
+_src_path: https://gitlab.softwareheritage.org/swh/devel/swh-py-template.git
+description: Software Heritage web UI
+distribution_name: swh-web
+have_cli: false
+have_workers: false
+package_root: swh/web
+project_name: swh.web
+python_minimal_version: '3.7'
+readme_format: rst
diff --git a/.gitignore b/.gitignore
index d82de44c1..5b16d985d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,20 +1,15 @@
+*.egg-info/
 *.pyc
-*.sw?
-*~
-\#*
-.\#*
-/.coverage
-/.coverage.*
+.coverage
 .eggs/
-resources/test/
+.hypothesis
+.mypy_cache
+.tox
 __pycache__
-version.txt
-swh.web.egg-info
 docs/build/
 docs/uri-scheme.md
 docs/dev-info.md
 *.sqlite3*
-.vscode/
 .directory
 node_modules/
 static/*.*
@@ -25,9 +20,6 @@ static/jssources/
 static/img/thirdParty/
 build/
 dist/
-.hypothesis
-.cache
-.pytest_cache
 .tox/
 .mypy_cache/
 package-lock.json
@@ -41,3 +33,4 @@ cypress/junit/
 cypress/downloads/
 .eslintcache
 docs/README.rst
+docs/README.md
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 4b295e380..ee848cd03 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -2,15 +2,24 @@ exclude: "^swh/web/tests/resources/"
 
 repos:
   - repo: https://github.com/pre-commit/pre-commit-hooks
-    rev: v4.3.0
+    rev: v4.4.0
     hooks:
       - id: trailing-whitespace
-        exclude: ".eml$"
       - id: check-json
       - id: check-yaml
 
+  - repo: https://github.com/python/black
+    rev: 23.1.0
+    hooks:
+      - id: black
+
+  - repo: https://github.com/PyCQA/isort
+    rev: 5.12.0
+    hooks:
+      - id: isort
+
   - repo: https://github.com/pycqa/flake8
-    rev: 5.0.4
+    rev: 6.0.0
     hooks:
       - id: flake8
         additional_dependencies: [flake8-bugbear==22.9.23]
@@ -45,16 +54,6 @@ repos:
         language: system
         types: [javascript]
 
-  - repo: https://github.com/PyCQA/isort
-    rev: 5.11.5
-    hooks:
-      - id: isort
-
-  - repo: https://github.com/python/black
-    rev: 22.10.0
-    hooks:
-      - id: black
-
   - repo: https://github.com/Riverside-Healthcare/djLint
     rev: v1.31.1
     hooks:
diff --git a/MANIFEST.in b/MANIFEST.in
deleted file mode 100644
index 7e69941a4..000000000
--- a/MANIFEST.in
+++ /dev/null
@@ -1,16 +0,0 @@
-include pytest.ini
-include README.md
-include requirements*.txt
-include tox.ini
-include version.txt
-recursive-include swh py.typed
-
-recursive-include assets *
-recursive-include swh/web/*/templates *
-recursive-include swh/web/*/tests/assets *
-recursive-include swh/web/*/tests/data *
-recursive-include swh/web/*/tests/resources *
-recursive-include swh/web/tests/resources *
-
-include package.json
-include yarn.lock
diff --git a/Makefile b/Makefile
index 5db623f03..524175c2b 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-# Makefile driver for swh Python modules. DO NOT CHANGE.
+# Makefile driver for SWH Python modules. DO NOT CHANGE.
 # You can add custom Makefile rules to Makefile.local
 
 include ../Makefile.python
diff --git a/docs/Makefile b/docs/Makefile
index ceee056eb..faf78f40f 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -1,3 +1,2 @@
-include ../../swh-docs/Makefile.sphinx
-
+include swh-docs/Makefile.sphinx
 APIDOC_EXCLUDES += ../assets ../cypress ../static ../swh/*/settings
diff --git a/mypy.ini b/mypy.ini
index 4d0608893..bb0b3ed5f 100644
--- a/mypy.ini
+++ b/mypy.ini
@@ -1,6 +1,10 @@
 [mypy]
 namespace_packages = True
 warn_unused_ignores = True
+explicit_package_bases = True
+# ^ Needed for mypy to detect py.typed from swh packages installed
+# in editable mode
+
 # support for django magic: https://github.com/typeddjango/django-stubs
 plugins = mypy_django_plugin.main, mypy_drf_plugin.main
 
@@ -8,7 +12,6 @@ plugins = mypy_django_plugin.main, mypy_drf_plugin.main
 django_settings_module = swh.web.settings.development
 
 # 3rd party libraries without stubs (yet)
-
 [mypy-bs4.*]
 ignore_missing_imports = True
 
@@ -42,9 +45,6 @@ ignore_missing_imports = True
 [mypy-pygments.*]
 ignore_missing_imports = True
 
-[mypy-pytest.*]
-ignore_missing_imports = True
-
 [mypy-requests_mock.*]
 ignore_missing_imports = True
 
@@ -56,3 +56,6 @@ ignore_missing_imports = True
 
 [mypy-swh.docs.*]
 ignore_missing_imports = True
+
+# [mypy-add_your_lib_here.*]
+# ignore_missing_imports = True
diff --git a/pyproject.toml b/pyproject.toml
index d4d28af19..cf0572f87 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,3 +1,45 @@
+[project]
+name = "swh.web"
+authors = [
+    {name="Software Heritage developers", email="swh-devel@inria.fr"},
+]
+
+description = "Software Heritage web UI"
+readme = {file = "README.rst", content-type = "text/x-rst"}
+requires-python = ">=3.7"
+classifiers = [
+    "Programming Language :: Python :: 3",
+    "Intended Audience :: Developers",
+    "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",
+    "Operating System :: OS Independent",
+    "Development Status :: 5 - Production/Stable",
+    "Framework :: Django",
+]
+dynamic = ["version", "dependencies", "optional-dependencies"]
+
+[tool.setuptools.packages.find]
+include = ["swh.*"]
+
+[tool.setuptools.dynamic]
+dependencies = {file = ["requirements.txt", "requirements-swh.txt"]}
+
+[tool.setuptools.dynamic.optional-dependencies]
+testing = {file = ["requirements-test.txt"]}
+
+[project.urls]
+"Homepage" = "https://gitlab.softwareheritage.org/swh/devel/swh-web"
+"Bug Reports" = "https://gitlab.softwareheritage.org/swh/devel/swh-web/-/issues"
+"Funding" = "https://www.softwareheritage.org/donate"
+"Documentation" = "https://docs.softwareheritage.org/devel/swh-web/"
+"Source" = "https://gitlab.softwareheritage.org/swh/devel/swh-web.git"
+
+[build-system]
+requires = ["setuptools", "setuptools-scm"]
+build-backend = "setuptools.build_meta"
+
+[tool.setuptools_scm]
+fallback_version = "0.0.1"
+
 [tool.black]
 target-version = ['py37']
 exclude = 'swh/web/tests/resources/contents'
diff --git a/pytest.ini b/pytest.ini
index b82ccd747..00c98462e 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -1,6 +1,11 @@
 [pytest]
-addopts = -p no:flask -p no:pytest_swh_storage -p swh.web.tests.pytest_plugin --ignore=swh/web/tests/random_fixtures_test.py
-norecursedirs = build docs node_modules .tox
+addopts =
+    -p no:flask
+    -p no:pytest_swh_storage
+    -p swh.web.tests.pytest_plugin
+    --ignore-glob=*random_fixtures_test.py
+    --ignore-glob=*create_test_*.py
+norecursedirs = build docs node_modules resources .tox
 DJANGO_SETTINGS_MODULE = swh.web.settings.tests
 filterwarnings =
     ignore:.*Plural value must be an integer, got float
diff --git a/setup.py b/setup.py
index 2b7fdac14..54e4e10b1 100755
--- a/setup.py
+++ b/setup.py
@@ -1,40 +1,12 @@
 #!/usr/bin/env python3
-# Copyright (C) 2015-2019  The Software Heritage developers
+# Copyright (C) 2015-2023  The Software Heritage developers
 # See the AUTHORS file at the top-level directory of this distribution
 # License: GNU Affero General Public License v3 or later
 # See top-level LICENSE file for more information
 
-from io import open
 import os
-from os import path
-
-from setuptools import find_packages, setup
-
-here = path.abspath(path.dirname(__file__))
-
-# Get the long description from the README file
-with open(path.join(here, "README.rst"), encoding="utf-8") as f:
-    long_description = f.read()
-
-
-def parse_requirements(name=None):
-    if name:
-        reqf = "requirements-%s.txt" % name
-    else:
-        reqf = "requirements.txt"
-
-    requirements = []
-    if not path.exists(reqf):
-        return requirements
-
-    with open(reqf) as f:
-        for line in f.readlines():
-            line = line.strip()
-            if not line or line.startswith("#"):
-                continue
-            requirements.append(line)
-    return requirements
 
+from setuptools import setup
 
 # package generated static assets as module data files
 data_files = []
@@ -43,35 +15,7 @@ for folder in ("static/", "assets/"):
         root_files = [os.path.join(root, i) for i in files]
         data_files.append((os.path.join("share/swh/web", root), root_files))
 
+# TODO: data_files usage has been deprecated, we should move away from using it...
 setup(
-    name="swh.web",
-    description="Software Heritage Web UI",
-    long_description=long_description,
-    long_description_content_type="text/x-rst",
-    python_requires=">=3.7",
-    author="Software Heritage developers",
-    author_email="swh-devel@inria.fr",
-    url="https://gitlab.softwareheritage.org/swh/devel/swh-web",
-    packages=find_packages(),
-    scripts=[],
-    install_requires=parse_requirements() + parse_requirements("swh"),
-    setup_requires=["setuptools-scm"],
-    use_scm_version=True,
-    extras_require={"testing": parse_requirements("test")},
-    include_package_data=True,
-    classifiers=[
-        "Programming Language :: Python :: 3",
-        "Intended Audience :: Developers",
-        "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",  # noqa
-        "Operating System :: OS Independent",
-        "Development Status :: 5 - Production/Stable",
-        "Framework :: Django",
-    ],
-    project_urls={
-        "Bug Reports": "https://gitlab.softwareheritage.org/swh/devel/swh-web/-/issues",
-        "Funding": "https://www.softwareheritage.org/donate",
-        "Source": "https://gitlab.softwareheritage.org/swh/devel/swh-web.git",
-        "Documentation": "https://docs.softwareheritage.org/devel/swh-web/",
-    },
     data_files=data_files,
 )
diff --git a/swh/__init__.py b/swh/__init__.py
deleted file mode 100644
index b36383a61..000000000
--- a/swh/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from pkgutil import extend_path
-
-__path__ = extend_path(__path__, __name__)
diff --git a/swh/web/settings/common.py b/swh/web/settings/common.py
index b2aaaf2e8..fc1b08db9 100644
--- a/swh/web/settings/common.py
+++ b/swh/web/settings/common.py
@@ -10,9 +10,11 @@ Django common settings for swh-web.
 
 from importlib.util import find_spec
 import os
+from pathlib import Path
 import site
 import sys
 from typing import Any, Dict
+import warnings
 
 from django.utils import encoding
 
@@ -198,15 +200,27 @@ STATIC_DIR = os.path.join(sys.prefix, "share/swh/web/static")
 if not os.path.exists(STATIC_DIR):
     # static folder location when swh-web has been installed with "pip --user"
     STATIC_DIR = os.path.join(site.getuserbase(), "share/swh/web/static")
+
+if not os.path.exists(STATIC_DIR):
+    ROOT_DIR = Path(PROJECT_DIR).absolute()
+    while not (ROOT_DIR / "static").is_dir():
+        ROOT_DIR = ROOT_DIR = ROOT_DIR.parent
+        if (ROOT_DIR / ".git").is_dir() or (ROOT_DIR / "pyproject.toml").is_file():
+            break
+        if ROOT_DIR == ROOT_DIR.parent:
+            break
+    STATIC_DIR = str(ROOT_DIR / "static")
+
 if not os.path.exists(STATIC_DIR):
-    # static folder location when developping swh-web
-    STATIC_DIR = os.path.join(PROJECT_DIR, "../../../static")
+    warnings.warn(
+        "Unable to find the static assets directory. Check your installation of swh.web."
+    )
+
 STATICFILES_DIRS = [STATIC_DIR]
 
 if "static_path" in SWH_MIRROR_CONFIG:
     STATICFILES_DIRS.append(SWH_MIRROR_CONFIG["static_path"])
 
-
 INTERNAL_IPS = ["127.0.0.1"]
 
 throttle_rates = {}
diff --git a/swh/web/utils/exc.py b/swh/web/utils/exc.py
index a3367ea89..ae4079c1f 100644
--- a/swh/web/utils/exc.py
+++ b/swh/web/utils/exc.py
@@ -81,7 +81,6 @@ http_status_code_message = {
 def _generate_error_page(
     request: HttpRequest, error_code: int, error_description: str
 ) -> HttpResponse:
-
     error_data = {
         "error": http_status_code_message[error_code],
         "reason": error_description,
diff --git a/tox.ini b/tox.ini
index fe028a604..e48d0e475 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,7 +1,10 @@
 [tox]
-requires =
-  tox>4
-envlist=black,flake8,mypy,py3
+minversion = 4
+envlist =
+  black
+  flake8
+  mypy
+  py3
 
 [testenv]
 extras =
@@ -10,10 +13,24 @@ deps =
   pytest-cov
 commands =
   pytest \
-    slow:  --hypothesis-profile=swh-web \
-    !slow: --hypothesis-profile=swh-web-fast \
-      --ignore-glob=*random_fixtures_test.py \
-      --cov {envsitepackagesdir}/swh/web --cov-branch {posargs} {envsitepackagesdir}/swh/web
+         --import-mode importlib \
+         --rootdir {envsitepackagesdir} \
+         --cov={envsitepackagesdir}/swh/web \
+         --cov-branch \
+  slow:  --hypothesis-profile=swh-web \
+  !slow: --hypothesis-profile=swh-web-fast \
+         {envsitepackagesdir}/swh/web \
+         {posargs}
+# --rootdir and --import-mode are required to make tests that depends
+# on the test file to be a proper submodule of the swh namespace after
+# migration to PEP420 (implicit namespace).
+
+[testenv:black]
+skip_install = true
+deps =
+  black==22.10.0
+commands =
+  {envpython} -m black --check swh
 
 [testenv:flake8]
 skip_install = true
@@ -34,44 +51,19 @@ deps =
 commands =
   mypy swh
 
-[testenv:black]
-skip_install = true
-deps =
-  black==22.10.0
-commands =
-  {envpython} -m black --exclude swh/web/tests/resources --check swh
-
 # build documentation outside swh-environment using the current
 # git HEAD of swh-docs, is executed on CI for each diff to prevent
 # breaking doc build
 [testenv:sphinx]
 allowlist_externals = make
-usedevelop = true
-extras =
-  testing
-deps =
-  # fetch and install swh-docs in develop mode
-  -e git+https://gitlab.softwareheritage.org/swh/devel/swh-docs.git\#egg=swh.docs
-setenv =
-  SWH_PACKAGE_DOC_TOX_BUILD = 1
-  # turn warnings into errors
-  SPHINXOPTS = -W
-commands =
-  make -I ../.tox/sphinx/src/swh-docs/swh/ -C docs
-
-# build documentation only inside swh-environment using local state
-# of swh-docs package
-[testenv:sphinx-dev]
-allowlist_externals = make
-usedevelop = true
 extras =
   testing
 deps =
-  # install swh-docs in develop mode
-  -e ../swh-docs
+  # fetch and install swh-docs
+  git+https://gitlab.softwareheritage.org/swh/devel/swh-docs.git\#egg=swh.docs
 setenv =
   SWH_PACKAGE_DOC_TOX_BUILD = 1
   # turn warnings into errors
   SPHINXOPTS = -W
 commands =
-  make -I ../.tox/sphinx-dev/src/swh-docs/swh/ -C docs
+  make -I {env_dir}/share/ -C docs
-- 
GitLab