Skip to content
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

Draft
wants to merge 16 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions grayskull/base/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@
from souschef.recipe import Recipe

from grayskull.strategy.cran import CranStrategy
from grayskull.strategy.py_build import PyBuild
from grayskull.strategy.pypi import PypiStrategy


class GrayskullFactory(ABC):
REGISTERED_STRATEGY = {
"pypi": PypiStrategy,
"pybuild": PyBuild,
"cran": CranStrategy,
}

Expand Down
1 change: 1 addition & 0 deletions grayskull/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class Configuration:
is_arch: bool = False
repo_github: Optional[str] = None
from_local_sdist: bool = False
from_tree: bool = False
local_sdist: Optional[str] = None
missing_deps: set = field(default_factory=set)
extras_require_test: Optional[str] = None
Expand Down
15 changes: 13 additions & 2 deletions grayskull/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@
from grayskull.cli import CLIConfig
from grayskull.cli.stdout import print_msg
from grayskull.config import Configuration
from grayskull.utils import generate_recipe, origin_is_github, origin_is_local_sdist
from grayskull.utils import (
generate_recipe,
origin_is_github,
origin_is_local_sdist,
origin_is_tree,
)

init(autoreset=True)
logging.basicConfig(format="%(levelname)s:%(message)s")
Expand Down Expand Up @@ -283,10 +288,13 @@ def generate_recipes_from_list(list_pkgs, args):
for pkg_name in list_pkgs:
logging.debug(f"Starting grayskull for pkg: {pkg_name}")
from_local_sdist = origin_is_local_sdist(pkg_name)
from_tree = origin_is_tree(pkg_name)
if origin_is_github(pkg_name):
pypi_label = ""
elif from_local_sdist:
pypi_label = " (local)"
elif from_tree:
pypi_label = " (tree)"
else:
pypi_label = " (pypi)"
print_msg(
Expand All @@ -304,6 +312,7 @@ def generate_recipes_from_list(list_pkgs, args):
url_pypi_metadata=args.url_pypi_metadata,
sections_populate=args.sections_populate,
from_local_sdist=from_local_sdist,
from_tree=from_tree,
extras_require_test=args.extras_require_test,
github_release_tag=args.github_release_tag,
extras_require_include=tuple(args.extras_require_include),
Expand Down Expand Up @@ -333,7 +342,9 @@ def create_python_recipe(pkg_name, sections_populate=None, **kwargs):
config = Configuration(name=pkg_name, **kwargs)
return (
GrayskullFactory.create_recipe(
"pypi", config, sections_populate=sections_populate
"pybuild" if config.from_tree else "pypi",
config,
sections_populate=sections_populate,
),
config,
)
Expand Down
80 changes: 80 additions & 0 deletions grayskull/strategy/py_build.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"""
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 packaging.markers import Marker
Fixed Show fixed Hide fixed
from packaging.metadata import Metadata
Fixed Show fixed Hide fixed
from souschef.recipe import Recipe

from grayskull.config import Configuration
from grayskull.strategy.abstract_strategy import AbstractStrategy

log = logging.getLogger(__name__)


class PyBuild(AbstractStrategy):
@staticmethod
def fetch_data(recipe: Recipe, config: Configuration, sections=None):
project = build.ProjectBuilder(config.name)

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)

recipe["requirements"]["host"] = sorted(
(*build_system_requires, *requires_for_build)
)

# 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

# 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}" %}}'
elif config.name not in recipe[0]:
log.warning("Package name not found in first line of recipe")

config.version = distribution.version
requires_dist = (
Fixed Show fixed Hide fixed
distribution.requires
) # includes extras as markers e.g. ; extra == 'testing'. Evaluate using Marker().
entry_points = (
Fixed Show fixed Hide fixed
distribution.entry_points
) # list(EntryPoint(name, value, group)

# 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()

# raise NotImplementedError()
8 changes: 7 additions & 1 deletion grayskull/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,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()
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that can work, but you also need to look for setup.cfg

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, but not all projects uses it :/

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 package.dist-info/METADATA. Then we read METADATA and not pyproject.toml. So with this technique we don't touch setup.py, setup.cfg or pyproject.toml; pypa/build writes METADATA and we read that.



def sha256_checksum(filename, block_size=65536):
sha256 = hashlib.sha256()
with open(filename, "rb") as f:
Expand Down Expand Up @@ -214,7 +220,7 @@ def generate_recipe(
recipe_dir = Path(folder_path) / pkg_name
logging.debug(f"Generating recipe on: {recipe_dir}")
if not recipe_dir.is_dir():
recipe_dir.mkdir()
recipe_dir.mkdir(parents=True)
recipe_path = recipe_dir / "meta.yaml"
recipe_folder = recipe_dir
add_new_lines_after_section(recipe.yaml)
Expand Down
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ dynamic = ["version"]
requires-python = ">=3.8"
dependencies = [
"beautifulsoup4",
"build", # conda install python-build
"colorama",
"conda-souschef >=2.2.3",
"packaging >=21.3",
Expand Down Expand Up @@ -52,8 +53,6 @@ docs = [

[project.scripts]
grayskull = "grayskull.main:main"
greyskull = "grayskull.main:main"
conda-grayskull = "grayskull.main:main"
conda-greyskull = "grayskull.main:main"

[project.urls]
Expand Down
Loading