-
-
Notifications
You must be signed in to change notification settings - Fork 69
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Generate recipe from project tree using pypa/build #541
base: main
Are you sure you want to change the base?
Changes from all commits
bb44c0e
09485b3
bbbf259
c712fe9
e967d1c
e262ec9
ad98bb6
8ef8e25
5f39732
ec43149
2734288
9ba91d8
c6d2934
a6ded19
523f893
16a8933
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
""" | ||
Use pypa/build to get project metadata from a checkout. Create a recipe suitable | ||
for inlinining into the first-party project source tree. | ||
""" | ||
|
||
import logging | ||
import tempfile | ||
from importlib.metadata import PathDistribution | ||
from pathlib import Path | ||
|
||
import build | ||
from conda.exceptions import InvalidMatchSpec | ||
from conda.models.match_spec import MatchSpec | ||
from packaging.requirements import Requirement | ||
from souschef.recipe import Recipe | ||
|
||
from grayskull.config import Configuration | ||
from grayskull.strategy.abstract_strategy import AbstractStrategy | ||
from grayskull.strategy.pypi import compose_test_section | ||
|
||
log = logging.getLogger(__name__) | ||
|
||
|
||
class PyBuild(AbstractStrategy): | ||
@staticmethod | ||
def fetch_data(recipe: Recipe, config: Configuration, sections=None): | ||
project = build.ProjectBuilder(config.name) | ||
|
||
recipe["source"]["path"] = "../" | ||
# XXX relative to output. "git_url: ../" is also a good choice. | ||
|
||
with tempfile.TemporaryDirectory(prefix="grayskull") as output: | ||
build_system_requires = project.build_system_requires | ||
requires_for_build = project.get_requires_for_build("wheel") | ||
# If those are already installed, we can get the extras requirements | ||
# without invoking pip e.g. setuptools_scm[toml] | ||
print("Requires for build:", build_system_requires, requires_for_build) | ||
|
||
# Example of finding extra dependencies for a distribution 'scm' (is | ||
# there a dict API?) Subtract "has non-extra marker" dependencies from | ||
# this set. | ||
# | ||
# Easier API is deprecated pkg_resources.Distribution().requires(extras=()) | ||
# | ||
# for e in scm.metadata.get_all('provides-extra'): | ||
# print (e, [x for x in r if x.marker and x.marker.evaluate({'extra':e})]) | ||
# | ||
# docs [<Requirement('entangled-cli[rich]; extra == "docs"')>, <Requirement('mkdocs; extra == "docs"')>, <Requirement('mkdocs-entangled-plugin; extra == "docs"')>, <Requirement('mkdocs-material; extra == "docs"')>, <Requirement('mkdocstrings[python]; extra == "docs"')>, <Requirement('pygments; extra == "docs"')>] | ||
# rich [<Requirement('rich; extra == "rich"')>] | ||
# test [<Requirement('build; extra == "test"')>, <Requirement('pytest; extra == "test"')>, <Requirement('rich; extra == "test"')>, <Requirement('wheel; extra == "test"')>] | ||
# toml [] | ||
|
||
# build the project's metadata "dist-info" directory | ||
metadata_path = Path(project.metadata_path(output_directory=output)) | ||
|
||
distribution = PathDistribution(metadata_path) | ||
|
||
# real distribution name not pathname | ||
config.name = distribution.name # see also _normalized_name | ||
config.version = distribution.version | ||
|
||
# grayskull thought the name was the path. correct that. | ||
if recipe[0] == '#% set name = "." %}': # XXX fragile | ||
# recipe[0] = x does not work | ||
recipe._yaml._yaml_get_pre_comment()[0].value = ( | ||
f'#% set name = "{config.name}" %}}\n' | ||
f'#% set version = "{config.version}" %}}\n' | ||
) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of that, you can use set_global_jinja_var(recipe, "name", config.name)
set_global_jinja_var(recipe, "version", config.version) |
||
|
||
recipe["package"]["version"] = "{{ version }}" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This puts quotes around There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. conda-souschef is just a wrapper around ruamel yaml, you can still manipulate the ruamel yaml object directly. recipe["package"]["version"].value = "{{ version }}" |
||
elif config.name not in str(recipe[0]): | ||
log.warning("Package name not found in first line of recipe") | ||
|
||
metadata = distribution.metadata | ||
|
||
requires_python = metadata["requires-python"] | ||
if requires_python: | ||
requires_python = f"python { requires_python }" | ||
else: | ||
requires_python = "python" | ||
|
||
recipe["requirements"]["host"] = [requires_python] + sorted( | ||
(*build_system_requires, *requires_for_build) | ||
) | ||
|
||
requirements = [Requirement(r) for r in distribution.requires or []] | ||
active_requirements = [ | ||
str(r).rsplit(";", 1)[0] | ||
for r in requirements | ||
if not r.marker or r.marker.evaluate() | ||
] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How do we include the marker as a YAML comment? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These are evaluated in the environment grayskull runs in, and might not be correct for the end user. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. there is a property called |
||
# XXX to normalize space between name and version, MatchSpec(r).spec | ||
normalized_requirements = [] | ||
for requirement in active_requirements: | ||
try: | ||
normalized_requirements.append( | ||
# MatchSpec uses a metaclass hiding its constructor from | ||
# the type checker | ||
MatchSpec(requirement).spec # type: ignore | ||
) | ||
except InvalidMatchSpec: | ||
log.warning("%s is not a valid MatchSpec", requirement) | ||
normalized_requirements.append(requirement) | ||
|
||
# conda does support ~=3.0.0 "compatibility release" matches | ||
recipe["requirements"]["run"] = [requires_python] + normalized_requirements | ||
# includes extras as markers e.g. ; extra == 'testing'. Evaluate | ||
# using Marker(). | ||
|
||
recipe["build"]["entry_points"] = [ | ||
f"{ep.name} = {ep.value}" | ||
for ep in distribution.entry_points | ||
if ep.group == "console_scripts" | ||
] | ||
|
||
recipe["build"]["noarch"] = "python" | ||
recipe["build"][ | ||
"script" | ||
] = "{{ PYTHON }} -m pip install . -vv --no-deps --no-build-isolation" | ||
# XXX also --no-index? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be nice to be careful about not fetching pip requirements, even the build-system requirements (poetry, flit-core, hatchling, ...) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sorry, I think I didn't follow, what do you mean? |
||
|
||
# distribution.metadata.keys() for grayskull is | ||
# Metadata-Version | ||
# Name | ||
# Version | ||
# Summary | ||
# Author-email | ||
# License | ||
# Project-URL | ||
# Keywords | ||
# Requires-Python | ||
# Description-Content-Type | ||
# License-File | ||
# License-File | ||
# Requires-Dist (many times) | ||
# Provides-Extra (several times) | ||
# Description or distribution.metadata.get_payload() | ||
|
||
about = { | ||
"summary": metadata["summary"], | ||
"license": metadata["license"], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On an example I tested, |
||
# there are two license-file in grayskull e.g. | ||
"license_file": metadata["license-file"], | ||
} | ||
recipe["about"] = about | ||
|
||
metadata_dict = dict( | ||
metadata | ||
) # XXX not what compose_test_section expects at all | ||
metadata_dict["name"] = config.name | ||
metadata_dict["entry_points"] = [ | ||
f"{ep.name} = {ep.value}" | ||
for ep in distribution.entry_points | ||
if ep.group == "console_scripts" | ||
] | ||
recipe["test"] = compose_test_section(metadata_dict, []) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe this sort of "copy all fields from one format to another" code is destined to be messy... |
||
|
||
# raise NotImplementedError() |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -109,6 +109,12 @@ def origin_is_local_sdist(name: str) -> bool: | |
) | ||
|
||
|
||
def origin_is_tree(name: str) -> bool: | ||
"""Return True if it is a directory""" | ||
path = Path(name) | ||
return path.is_dir() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could look for pyproject.toml or setup.py; it should work with either. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. that can work, but you also need to look for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With the pypa/build method, pyproject.toml is the only necessary file to get the metadata. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree, but not all projects uses it :/ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. pypa/build works with setup.py or pyproject.toml. It asks the underlying build system to create a wheel with |
||
|
||
|
||
def sha256_checksum(filename, block_size=65536): | ||
sha256 = hashlib.sha256() | ||
with open(filename, "rb") as f: | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having to import conda will make grayskull more difficult to install.
Is it worth adding conda as dependency just for that function?
I haven't checked how requirements are parsed in the current code.