Skip to content

Commit

Permalink
Merge pull request #98 from zuzukin/unit-tests
Browse files Browse the repository at this point in the history
Unit tests
  • Loading branch information
analog-cbarber committed Sep 17, 2023
2 parents e21918a + 83f68fa commit 7f57e50
Show file tree
Hide file tree
Showing 10 changed files with 311 additions and 65 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ help:
DEV_INSTALL := $(CONDA_RUN) pip install -e . --no-deps --no-build-isolation

createdev:
conda env create -f environment.yml -n $(DEV_ENV)
conda env create -f environment.yml -n $(DEV_ENV) --yes
$(MAKE) dev-install

updatedev:
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
[![documentation](https://img.shields.io/badge/docs-mkdocs%20material-blue.svg?style=flat)](https://zuzukin.github.io/whl2conda/)
![PyPI - Python Version](https://img.shields.io/pypi/pyversions/whl2conda)
![GitHub](https://img.shields.io/github/license/analog-cbarber/whl2conda)
[![CI](https://github.com/zuzukin/whl2conda/actions/workflows/python-package-conda.yml/badge.svg)](https://github.com/zuzukin/whl2conda/actions/workflows/python-package-conda.yml)
[![CI](https://github.com/zuzukin/whl2conda/actions/workflows/python-package-conda.yml/badge.svg)](https://github.com/zuzukin/whl2conda/actions/workflows/python-package-conda.yml) [![codecov](https://codecov.io/gh/zuzukin/whl2conda/graph/badge.svg?token=097C3MBNIX)](https://codecov.io/gh/zuzukin/whl2conda)
![GitHub issues](https://img.shields.io/github/issues/analog-cbarber/whl2conda)


**Generate conda packages directly from pure python wheels**

*whl2conda* is a command line utility to build and test conda packages
Expand Down
91 changes: 41 additions & 50 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 @@ -399,8 +389,8 @@ def _write_link_file(self, conda_info_dir: Path, wheel_info_dir: Path) -> None:
wheel_entry_points.read(wheel_entry_points_file)
for section_name in ["console_scripts", "gui_scripts"]:
if section_name in wheel_entry_points:
if section := wheel_entry_points[section_name]:
console_scripts.extend(f"{k}={v}" for k, v in section.items())
section = wheel_entry_points[section_name]
console_scripts.extend(f"{k}={v}" for k, v in section.items())
noarch_dict: dict[str, Any] = dict(type="python")
if console_scripts:
noarch_dict["entry_points"] = console_scripts
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 @@ -663,8 +660,6 @@ def _parse_wheel_metadata(self, wheel_dir: Path) -> MetadataFromWheel:
md.setdefault(mdkey.lower(), []).append(mdval)
else:
md[mdkey.lower()] = mdval
if mdkey in {"requires-dist", "requires"}:
continue

requires: list[RequiresDistEntry] = []
raw_requires_entries = md.get("requires-dist", md.get("requires", ()))
Expand Down Expand Up @@ -716,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 @@ -736,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.
#
45 changes: 45 additions & 0 deletions test-projects/setup/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# 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",
],
extras_require = {
'bdev': [ 'black' ]
},
packages=["mypkg"]
)
Loading

0 comments on commit 7f57e50

Please sign in to comment.