Switch to side-by-side view

--- a
+++ b/tests/utils/test_package.py
@@ -0,0 +1,213 @@
+import importlib
+import subprocess
+import sys
+import tarfile
+import zipfile
+
+import pytest
+
+import edsnlp
+from edsnlp.package import package
+
+
+def test_blank_package(nlp, tmp_path):
+    # Missing metadata makes poetry fail due to missing author / description
+    if not isinstance(nlp, edsnlp.Pipeline):
+        pytest.skip("Only running for edsnlp.Pipeline")
+
+    package(
+        pipeline=nlp,
+        root_dir=tmp_path,
+        name="test-model-fail",
+        metadata={},
+        project_type="poetry",
+    )
+
+    nlp.package(
+        root_dir=tmp_path,
+        name="test-model",
+        metadata={
+            "description": "A test model",
+            "authors": "Test Author <test.author@mail.com>",
+        },
+        project_type="poetry",
+        distributions=["wheel"],
+    )
+    assert (tmp_path / "dist").is_dir()
+    assert (tmp_path / "dist" / "test_model-0.1.0-py3-none-any.whl").is_file()
+    assert not (tmp_path / "dist" / "test_model-0.1.0.tar.gz").is_file()
+
+
+@pytest.mark.parametrize("package_name", ["my-test-model", None])
+@pytest.mark.parametrize("manager", ["poetry", "setuptools"])
+def test_package_with_files(nlp, tmp_path, package_name, manager):
+    if not isinstance(nlp, edsnlp.Pipeline):
+        pytest.skip("Only running for edsnlp.Pipeline")
+
+    nlp.to_disk(tmp_path / "model", exclude=set())
+
+    ((tmp_path / "test_model").mkdir(parents=True))
+    (tmp_path / "test_model" / "__init__.py").write_text('print("Hello World!")\n')
+    (tmp_path / "test_model" / "empty_folder").mkdir()
+    (tmp_path / "README.md").write_text(
+        """\
+<!-- INSERT -->
+# Test Model
+"""
+    )
+    if manager == "poetry":
+        (tmp_path / "pyproject.toml").write_text(
+            """\
+[build-system]
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"
+
+[tool.poetry]
+name = "test-model"
+version = "0.0.0"
+description = "A test model"
+authors = ["Test Author <test.author@mail.com>"]
+readme = "README.md"
+
+[tool.poetry.dependencies]
+python = ">=3.7"
+build = "*"  # sample light package to install
+"""
+        )
+    elif manager == "setuptools":
+        (tmp_path / "pyproject.toml").write_text(
+            """\
+[build-system]
+requires = ["setuptools>=42", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "test-model"
+version = "0.0.0"
+description = "A test model"
+authors = [
+    {name = "Test Author", email = "test.author@mail.com"}
+]
+readme = "README.md"
+requires-python = ">=3.7"
+
+dependencies = [
+    "build"
+]
+"""
+        )
+    package(
+        name=package_name,
+        pipeline=tmp_path / "model",
+        root_dir=tmp_path,
+        check_dependencies=False,
+        version="0.1.0",
+        distributions=None,
+        metadata={
+            "description": "A new description",
+            "authors": "Test Author <test.author@mail.com>",
+        },
+        readme_replacements={
+            "<!-- INSERT -->": "Replaced !",
+        },
+    )
+
+    module_name = "test_model" if package_name is None else "my_test_model"
+
+    assert (tmp_path / "dist").is_dir()
+    assert (tmp_path / "dist" / f"{module_name}-0.1.0.tar.gz").is_file()
+    assert (tmp_path / "dist" / f"{module_name}-0.1.0-py3-none-any.whl").is_file()
+    assert (tmp_path / "pyproject.toml").is_file()
+
+    with zipfile.ZipFile(
+        tmp_path / "dist" / f"{module_name}-0.1.0-py3-none-any.whl"
+    ) as zf:
+        # check files
+        assert set(zf.namelist()) == {
+            f"{module_name}-0.1.0.dist-info/METADATA",
+            f"{module_name}-0.1.0.dist-info/RECORD",
+            f"{module_name}-0.1.0.dist-info/WHEEL",
+            f"{module_name}/__init__.py",
+            f"{module_name}/artifacts/config.cfg",
+            f"{module_name}/artifacts/meta.json",
+            f"{module_name}/artifacts/tokenizer",
+            "test_model/__init__.py",
+        }
+        # check description
+        with zf.open(f"{module_name}-0.1.0.dist-info/METADATA") as f:
+            assert b"A new description" in f.read()
+
+    with tarfile.open(tmp_path / "dist" / f"{module_name}-0.1.0.tar.gz") as tf:
+        # check files
+        assert set(tf.getnames()) == {
+            f"{module_name}-0.1.0/PKG-INFO",
+            f"{module_name}-0.1.0/README.md",
+            f"{module_name}-0.1.0/artifacts/config.cfg",
+            f"{module_name}-0.1.0/artifacts/meta.json",
+            f"{module_name}-0.1.0/artifacts/tokenizer",
+            f"{module_name}-0.1.0/{module_name}/__init__.py",
+            f"{module_name}-0.1.0/pyproject.toml",
+            f"{module_name}-0.1.0/test_model/__init__.py",
+        }
+        # check description
+        with tf.extractfile(f"{module_name}-0.1.0/PKG-INFO") as f:
+            assert b"A new description" in f.read()
+
+        with tf.extractfile(f"{module_name}-0.1.0/README.md") as f:
+            assert b"Replaced !" in f.read()
+
+    # pip install the whl file
+    (tmp_path / "site-packages").mkdir(exist_ok=True)
+    subprocess.check_output(
+        [
+            sys.executable,
+            "-m",
+            "pip",
+            "install",
+            "-vvv",
+            "--target",
+            str(tmp_path / "site-packages"),
+            str(tmp_path / "dist" / f"{module_name}-0.1.0-py3-none-any.whl"),
+        ],
+        stderr=subprocess.STDOUT,
+    )
+
+    site_packages = tmp_path / "site-packages"
+    sys.path.insert(0, str(site_packages))
+    # check site-package files
+    files = {str(f.relative_to(site_packages)) for f in set(site_packages.rglob("*"))}
+    assert files >= {
+        f"{module_name}/artifacts",
+        f"{module_name}/artifacts/config.cfg",
+        f"{module_name}/artifacts/meta.json",
+        f"{module_name}/artifacts/tokenizer",
+    }
+
+    module = importlib.import_module(module_name)
+
+    with open(module.__file__) as f:
+        assert f.read() == (
+            ('print("Hello World!")\n' if package_name is None else "")
+            + """
+# -----------------------------------------
+# This section was autogenerated by edsnlp
+# -----------------------------------------
+
+import edsnlp
+from pathlib import Path
+from typing import Optional, Dict, Any
+
+__version__ = '0.1.0'
+
+def load(
+    overrides: Optional[Dict[str, Any]] = None,
+) -> edsnlp.Pipeline:
+    path_outside = Path(__file__).parent / "../artifacts"
+    path_inside = Path(__file__).parent / "artifacts"
+    path = path_inside if path_inside.exists() else path_outside
+    model = edsnlp.load(path, overrides=overrides)
+    return model
+"""
+        )
+    module.load()
+    edsnlp.load(module_name)