"""
Tools for metamodern software packaging.
Package detail discovery and automated setup.
"""
import inspect
import pathlib
import re
import subprocess
import typing
from importlib.machinery import SourceFileLoader
import pydeps
import toml
# XXX from pkg_resources import DistributionNotFound
# XXX from pkg_resources import iter_entry_points as get_entry_points
# XXX from .install import add, remove
# XXX from .listing import get_distribution
# XXX from .system import get_apt_history
__all__ = [
# XXX "DistributionNotFound",
# XXX "get_entry_points",
"auto_discover",
"discover",
"get_repo_files",
# XXX "add",
# XXX "remove",
# XXX "get_distribution",
# XXX "get_apt_history",
]
currently_discovering = False
def get_current_project(project_dir=".") -> typing.MutableMapping:
"""Return a dict of `pyproject.toml` in `project_dir`."""
with (pathlib.Path(project_dir) / "pyproject.toml").open() as fp:
return toml.load(fp)
def get_current_packages(project_dir=".") -> list:
"""Return a list of `pyproject.toml` in `project_dir`."""
project = get_current_project(project_dir)["tool"]["poetry"]
try:
packages = [p["include"] for p in project["packages"]]
except KeyError:
project_name = project["name"].replace(".", "_")
if not (pathlib.Path(project_dir) / project_name).exists():
project_name = f"{project_name}.py"
packages = [project_name]
return packages
def strip_local_dev_deps(project_dir="."):
"""Remove path-based development dependencies and add gmpg."""
pyproject_path = pathlib.Path(project_dir) / "pyproject.toml"
try:
with pyproject_path.open() as fp:
pyproject = toml.load(fp)
except FileNotFoundError:
return
try:
dev_deps = pyproject["tool"]["poetry"]["group"]["dev"]["dependencies"]
except KeyError:
return
for dep_name, dep_location in dict(dev_deps).items():
if isinstance(dep_location, dict):
if "path" in dep_location:
dev_deps.pop(dep_name)
dev_deps["gmpg"] = ">=0.1"
with pyproject_path.open("w") as fp:
toml.dump(pyproject, fp)
def detail_package(self):
"""
a knowledge tree extension for detailing the contents of Python packages
"""
packages = []
def handle(file):
z = discover(file)
print(z)
yield
print("XXX")
yield
def summarize():
print("{} packages found: {}".format(len(packages), ", ".join(packages)))
return handle, summarize
class PackageRepoError(Exception):
"""
raised when there exists a halting flaw in the package design
"""
def discover(pkgdir: str) -> dict:
"""
return a dictionary containing package details discovered at `pkgdir`
"""
# TODO gpg verify
# TODO author from first commit and maintainer from last tag's commit
# TODO url=`hg paths default`; verify against ^https://{gpg.comment}/
# TODO dirty versions
# TODO long_description = inspect.getdoc(setup_mod)
# TODO kwargs["package_data"] = {"": ["*.dat", "*.json", "*.yaml"]}
pkgdir = pathlib.Path(pkgdir)
if pkgdir.name == "setup.py":
pkgdir = pkgdir.parent
import setuptools
global discover
currently_supplied_args = None
def get_supplied(**args):
nonlocal currently_supplied_args
currently_supplied_args = args
_setup, setuptools.setup = setuptools.setup, get_supplied
_discover, discover = discover, lambda x, **y: {}
_setup_loader = SourceFileLoader("setup", str(pkgdir / "setup.py"))
setup_mod = _setup_loader.load_module()
setuptools.setup, discover = _setup, _discover
comments = inspect.getcomments(setup_mod)
name = re.match(r"^# \[`(.*?)`\]\[1\]", comments).groups()[0]
description = "TODO use setup.py docstring"
# XXX re.match(r"^# \[`.*`\]\[1\]: (.*)", comments).groups()[0]
license_match = re.search(r"%\[([A-Za-z ]+)\]", comments)
try:
license = license_match.groups()[0]
except AttributeError:
license = "Unknown"
url = re.search(r"^# \[1\]: (.*)$", comments, re.M).groups()[0]
if url.startswith("//"):
url = "https:" + url
download_url = "{}.git".format(url)
install_requires = currently_supplied_args.get("requires", [])
entry_points = currently_supplied_args.get("provides", {})
try:
entry_points["console_scripts"] = entry_points["term.apps"]
except KeyError:
pass
versions = gitsh("tag -l --sort -version:refname", pkgdir)
version = versions.splitlines()[0].lstrip("v") if versions else "0.0"
committers = gitsh(
"--no-pager log --no-color | grep " '"^Author: " --color=never', pkgdir
).splitlines()
def get_committer(index):
return re.match(r"Author: (.*) <(.*)>", committers[index]).groups()
author, author_email = get_committer(-1)
maintainer, maintainer_email = get_committer(0)
packages = setuptools.find_packages(str(pkgdir))
py_modules = [
p.stem for p in pkgdir.iterdir() if p.suffix == ".py" and p.stem != "setup"
]
kwargs = {}
if packages:
kwargs["packages"] = packages
if py_modules:
kwargs["py_modules"] = py_modules
return dict(
name=name,
version=version,
description=description,
url=url,
download_url=download_url,
install_requires=install_requires,
entry_points=entry_points,
license=license,
author=author,
author_email=author_email,
maintainer=maintainer,
maintainer_email=maintainer_email,
**kwargs,
)
def auto_discover(dist, _, setup_file):
"""
a `distutils` setup keyword for automatic discovery using `discover`
>>> import setuptools # doctest: +SKIP
>>> setuptools.setup(discover=__file__) # doctest: +SKIP
"""
global currently_discovering
currently_discovering = True
details = discover(setup_file)
dist.packages = details.pop("packages", [])
dist.py_modules = details.pop("py_modules", [])
# dist.install_requires = details.pop("requires", [])
# dist.entry_points = details.pop("provides")
dist.metadata.author = details.get("author", "")
dist.metadata.author_email = details.get("author_email", "")
dist.__dict__.update(details)
dist.metadata.__dict__.update(details)
def get_repo_files(setup_dir):
"""
a `setuptools` file finder for finding installable files from a Git repo
"""
if not currently_discovering:
return []
if not setup_dir:
setup_dir = "."
return gitsh("ls-files", setup_dir)
def gitsh(command, working_dir):
"""
return the output of running Git `command` in `working_dir`
"""
raw_cmd = "git -C {} {}".format(working_dir, command)
try:
return subprocess.check_output(
raw_cmd, stderr=subprocess.STDOUT, shell=True
).decode("utf-8")
except subprocess.CalledProcessError:
raise PackageRepoError("no Git repo at `{}`".format(working_dir))