Skip to content

Commit

Permalink
More unit tests (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
analog-cbarber committed Sep 17, 2023
1 parent 48ed969 commit ddb8660
Show file tree
Hide file tree
Showing 8 changed files with 239 additions and 63 deletions.
85 changes: 39 additions & 46 deletions src/whl2conda/api/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ def parse(cls, raw: str) -> RequiresDistEntry:
for pat in _extra_marker_re:
if m := pat.search(marker):
entry.extra_marker_name = m.group("name")
if m.string == marker:
if m.group(0) == marker:
entry.generic = True
break
return entry
Expand Down Expand Up @@ -250,8 +250,6 @@ class Wheel2CondaConverter:
conda_pkg_path: Optional[Path] = None
std_renames: dict[str, str]

temp_dir: Optional[tempfile.TemporaryDirectory] = None

def __init__(
self,
wheel_path: Path,
Expand All @@ -265,13 +263,6 @@ def __init__(
# TODO - option to ignore this
self.std_renames = load_std_renames()

def __enter__(self):
self.temp_dir = tempfile.TemporaryDirectory(prefix="whl2conda-")

def __exit__(self, exc_type, exc_val, exc_tb):
if self.temp_dir:
self.temp_dir.cleanup()

def convert(self) -> Path:
"""
Convert wheel to conda package
Expand All @@ -283,14 +274,13 @@ def convert(self) -> Path:
"""
# pylint: disable=too-many-statements,too-many-branches,too-many-locals

with self:
assert self.temp_dir is not None

extracted_wheel_dir = self._extract_wheel()
with tempfile.TemporaryDirectory(prefix="whl2conda-") as temp_dirname:
temp_dir = Path(temp_dirname)
extracted_wheel_dir = self._extract_wheel(temp_dir)

wheel_md = self._parse_wheel_metadata(extracted_wheel_dir)

conda_dir = Path(self.temp_dir.name).joinpath("conda-files")
conda_dir = temp_dir / "conda-files"
conda_info_dir = conda_dir.joinpath("info")
conda_dir.mkdir()

Expand Down Expand Up @@ -466,6 +456,7 @@ def _write_hash_input(self, conda_info_dir: Path) -> None:

# pylint: disable=too-many-locals
def _write_about(self, conda_info_dir: Path, md: dict[str, Any]) -> None:
"""Write the info/about.json file"""
# * info/about.json
#
# Note that the supported fields in the about section are not
Expand All @@ -480,41 +471,47 @@ def _write_about(self, conda_info_dir: Path, md: dict[str, Any]) -> None:
#
# conda-build also adds conda-build-version and conda-version fields.

license = md.get("license-expression") or md.get("license")
conda_about_file = conda_info_dir.joinpath("about.json")
# TODO description can come from METADATA message body
# then need to also use content type. It doesn't seem
# that conda-forge packages include this in the info/
doc_url: Optional[str] = None
dev_url: Optional[str] = None
extra: dict[str, Any] = non_none_dict(

conda_about_file = conda_info_dir.joinpath("about.json")

extra = non_none_dict(
author=md.get("author"),
classifiers=md.get("classifier"),
maintainer=md.get("maintainer"),
whl2conda_version=__version__,
)

proj_url_pat = re.compile(r"\s*(?P<key>\w+)\s*,\s*(?P<url>\w.*)\s*")
doc_url: Optional[str] = None
dev_url: Optional[str] = None
for urlline in md.get("project-url", ()):
urlparts = re.split(r"\s*,\s*", urlline, maxsplit=1)
if len(urlparts) > 1:
key, url = urlparts
keyl = key.lower()
if re.match(r"doc(umentation)?\b", keyl):
doc_url = urlparts[1]
elif re.match(r"(dev(elopment)?|repo(sitory))\b", keyl):
dev_url = urlparts[1]
if key and url:
extra[key] = url
if m := proj_url_pat.match(urlline): # pragma: no branch
key = m.group("key")
url = m.group("url")
if re.match(r"(?i)doc(umentation)?\b", key):
doc_url = url
elif re.match(r"(?i)(dev(elopment)?|repo(sitory))\b", key):
dev_url = url
extra[key] = url

for key in ["author-email", "maintainer-email"]:
val = md.get(key)
if val:
author_key = key.split("-", maxsplit=1)[0] + "s"
extra[author_key] = val.split(",")

license = md.get("license-expression") or md.get("license")
if license_files := md.get("license-file"):
extra["license_files"] = list(license_files)

if keywords := md.get("keywords"):
keyword_list = keywords.split(",")
else:
keyword_list = None

conda_about_file.write_text(
json.dumps(
non_none_dict(
Expand All @@ -525,9 +522,10 @@ def _write_about(self, conda_info_dir: Path, md: dict[str, Any]) -> None:
home=md.get("home-page"),
dev_url=dev_url,
doc_url=doc_url,
extra=extra or None,
extra=extra,
),
indent=2,
sort_keys=True,
)
)

Expand All @@ -538,8 +536,6 @@ def _compute_conda_dependencies(
) -> list[str]:
conda_dependencies: list[str] = []

# TODO - instead RequiresDistEntrys should be passed as an argument

for entry in dependencies:
if entry.extra_marker_name:
self._debug("Skipping extra dependency: %s", entry)
Expand Down Expand Up @@ -618,13 +614,14 @@ def _copy_licenses(self, conda_info_dir: Path, wheel_md: MetadataFromWheel) -> N
from_files = [wheel_info_dir / license_path.name]
if not license_path.is_absolute():
from_files.insert(0, wheel_info_dir / license_path)
for from_file in from_files:
if from_file.exists():
to_file = to_license_dir / from_file.relative_to(wheel_info_dir)
if not to_file.exists():
to_file.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(from_file, to_file)
break
for from_file in filter(
lambda f: f.exists(), from_files
): # pragma: no branch
to_file = to_license_dir / from_file.relative_to(wheel_info_dir)
if not to_file.exists(): # pragma: no branch
to_file.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(from_file, to_file)
break

# pylint: disable=too-many-locals, too-many-statements
def _parse_wheel_metadata(self, wheel_dir: Path) -> MetadataFromWheel:
Expand Down Expand Up @@ -714,11 +711,10 @@ def _parse_wheel_metadata(self, wheel_dir: Path) -> MetadataFromWheel:
)
return self.wheel_md

def _extract_wheel(self) -> Path:
def _extract_wheel(self, temp_dir: Path) -> Path:
self.logger.info("Reading %s", self.wheel_path)
wheel = WheelFile(self.wheel_path)
assert self.temp_dir
wheel_dir = Path(self.temp_dir.name).joinpath("wheel-files")
wheel_dir = temp_dir / "wheel-files"
wheel.extractall(wheel_dir)
if self.logger.getEffectiveLevel() <= logging.DEBUG:
for wheel_file in wheel_dir.glob("**/*"):
Expand All @@ -734,6 +730,3 @@ def _info(self, msg, *args):

def _debug(self, msg, *args):
self.logger.debug(msg, *args)

def _trace(self, msg, *args):
self.logger.log(logging.DEBUG - 5, msg, *args)
1 change: 1 addition & 0 deletions test-projects/setup/LICENSE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
some license
1 change: 1 addition & 0 deletions test-projects/setup/LICENSE2.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
another license
14 changes: 14 additions & 0 deletions test-projects/setup/mypkg/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Copyright 2023 Christopher Barber
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
43 changes: 43 additions & 0 deletions test-projects/setup/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Copyright 2023 Christopher Barber
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

import os
from setuptools import setup

setup(
name = "mypkg",
version = "1.3.4",
description = "Test package using setup.py",
author = "John Doe",
author_email="jdoe@nowhere.com",
classifiers = [
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
],
keywords=["python","test"],
maintainer = "Zuzu",
maintainer_email= "zuzu@nowhere.com",
license_files=[
"LICENSE.md",
os.path.abspath("LICENSE2.rst"),
],
install_requires = [
"tables",
"wheel",
"black; extra == 'dev'",
],
packages=["mypkg"]
)
72 changes: 62 additions & 10 deletions test/api/test_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,17 @@
Wheel2CondaConverter,
CondaPackageFormat,
DependencyRename,
RequiresDistEntry, Wheel2CondaError,
RequiresDistEntry,
Wheel2CondaError,
)
from whl2conda.cli.convert import do_build_wheel
from whl2conda.cli.install import install_main
from .validator import PackageValidator

from ..test_packages import simple_wheel # pylint: disable=unused-import
from ..test_packages import ( # pylint: disable=unused-import
setup_wheel,
simple_wheel,
)

this_dir = Path(__file__).parent.absolute()
root_dir = this_dir.parent.parent
Expand Down Expand Up @@ -73,7 +77,9 @@ class ConverterTestCase:

@property
def converter(self) -> Wheel2CondaConverter:
"""Converter instance. Only valid after build() is called"""
"""Converter instance, constructed on demand."""
if self._converter is None:
self._converter = Wheel2CondaConverter(self._get_wheel(), self.out_dir)
assert self._converter is not None
return self._converter

Expand Down Expand Up @@ -114,7 +120,7 @@ def build(self, out_format: CondaPackageFormat = CondaPackageFormat.V2) -> Path:
"""Run the build test case"""
self.was_run = True
wheel_path = self._get_wheel()
package_path = self._convert(wheel_path, out_format=out_format)
package_path = self._convert(out_format=out_format)
self._validate(wheel_path, package_path)
return package_path

Expand All @@ -124,8 +130,8 @@ def install(self, pkg_file: Path) -> Path:
install_main([str(pkg_file), "-p", str(test_env), "--yes", "--create"])
return test_env

def _convert(self, wheel_path: Path, *, out_format: CondaPackageFormat) -> Path:
converter = Wheel2CondaConverter(wheel_path, self.out_dir)
def _convert(self, *, out_format: CondaPackageFormat) -> Path:
converter = self.converter
converter.dependency_rename = list(self.dependency_rename)
converter.extra_dependencies = list(self.extra_dependencies)
converter.package_name = self.package_name
Expand Down Expand Up @@ -160,7 +166,12 @@ def _get_wheel(self) -> Path:
def _validate(self, wheel_path: Path, package_path: Path) -> None:
converter = self._converter
assert converter is not None
self._validator(wheel_path, package_path, std_renames=converter.std_renames)
self._validator.validate(
wheel_path,
package_path,
std_renames=converter.std_renames,
keep_pip_dependencies=converter.keep_pip_dependencies,
)


class ConverterTestCaseFactory:
Expand Down Expand Up @@ -278,6 +289,26 @@ def test_requires_dist_entry() -> None:
assert not entry4.extras
check_dist_entry(entry4)

entry5 = RequiresDistEntry.parse("sam ; python_version >= '3.10' ")
assert entry5.name == "sam"
assert entry5.marker == "python_version >= '3.10'"
assert not entry5.extra_marker_name
assert not entry5.version
assert not entry5.extras
assert not entry5.generic
check_dist_entry(entry5)

entry6 = RequiresDistEntry.parse(
"bilbo ~=23.2 ; sys_platform=='win32' and extra=='dev' "
)
assert entry6.name == "bilbo"
assert entry6.version == "~=23.2"
assert not entry6.extras
assert entry6.marker == "sys_platform=='win32' and extra=='dev'"
assert entry6.extra_marker_name == "dev"
assert not entry6.generic
check_dist_entry(entry6)

with pytest.raises(SyntaxError):
RequiresDistEntry.parse("=123 : bad")

Expand Down Expand Up @@ -340,6 +371,26 @@ def test_this(test_case: ConverterTestCaseFactory) -> None:
case.build(fmt)


def test_setup_wheel(
test_case: ConverterTestCaseFactory,
setup_wheel: Path,
) -> None:
"""Tests converting wheel from 'setup' project"""
v2pkg = test_case(setup_wheel).build()
assert v2pkg.suffix == ".conda"


def test_keep_pip_dependencies(
test_case: ConverterTestCaseFactory,
setup_wheel: Path,
) -> None:
"""Test keeping pip dependencies in dist-info"""
case = test_case(setup_wheel)
case.converter.keep_pip_dependencies = True
v1pkg = case.build(out_format=CondaPackageFormat.V1)
assert v1pkg.name.endswith(".tar.bz2")


def test_simple_wheel(
test_case: ConverterTestCaseFactory,
simple_wheel: Path,
Expand Down Expand Up @@ -404,6 +455,9 @@ def test_bad_wheels(
simple_wheel: Path,
tmp_path: Path,
) -> None:
"""
Test wheels that cannot be converter
"""
good_wheel = WheelFile(simple_wheel)
extract_dir = tmp_path / "extraxt"
good_wheel.extractall(str(extract_dir))
Expand Down Expand Up @@ -445,7 +499,7 @@ def test_bad_wheels(

#
# bad metadata version
3
#

WHEEL_msg.replace_header("Root-Is-Purelib", "True")
WHEEL_file.write_text(WHEEL_msg.as_string())
Expand All @@ -462,5 +516,3 @@ def test_bad_wheels(

with pytest.raises(Wheel2CondaError, match="unsupported metadata version"):
test_case(bad_md_version_wheel).build()


Loading

0 comments on commit ddb8660

Please sign in to comment.