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

Add plugin system and local wheels support #91

Merged
merged 55 commits into from
Jan 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
209e30f
Initial commit adding a plugin system and a PySide6 plugin
JeanChristopheMorinPerso Feb 3, 2024
b503662
Fix bug with requirements of merged packages
JeanChristopheMorinPerso Feb 4, 2024
7006ab9
Simplify registration of builtin plugins
JeanChristopheMorinPerso Feb 4, 2024
5a7ff04
Add pluggy to project deps
JeanChristopheMorinPerso Feb 4, 2024
d854773
Fix tests
JeanChristopheMorinPerso Feb 4, 2024
f41fb6d
Fix remaining issues (I think)
JeanChristopheMorinPerso Apr 28, 2024
2915723
Fix download tests and add a test for skipped downloads
JeanChristopheMorinPerso Apr 28, 2024
d184853
Fix typing
JeanChristopheMorinPerso Apr 29, 2024
5bd4d54
Fix typing
JeanChristopheMorinPerso Apr 29, 2024
f93a87d
Add basic test for plugins module
JeanChristopheMorinPerso Apr 29, 2024
a8fa6d0
Fix typing again
JeanChristopheMorinPerso Apr 29, 2024
91c7aca
Add type hints and new dataclasses to express plugin authors that som…
JeanChristopheMorinPerso Dec 31, 2024
788fb4f
Improve plugins docs and autogenerate signatures
JeanChristopheMorinPerso Dec 31, 2024
13667aa
Explicitly use tuples in hookspecs to improve typing even more and en…
JeanChristopheMorinPerso Dec 31, 2024
b798d8d
Fix some mypy errors
JeanChristopheMorinPerso Dec 31, 2024
8e53cf3
More typing fixes and fix regression introduced by calling hooks with…
JeanChristopheMorinPerso Jan 1, 2025
00f9ba9
More docs improvements for plugins
JeanChristopheMorinPerso Jan 1, 2025
5f6aca9
Add more docstrings
JeanChristopheMorinPerso Jan 1, 2025
c620857
Fix tests
JeanChristopheMorinPerso Jan 1, 2025
a453bcf
More typing fixes
JeanChristopheMorinPerso Jan 1, 2025
c37fe73
Fix all mypy errors
JeanChristopheMorinPerso Jan 1, 2025
d39e1d7
Add missing __eq__ method in PackageGroup for tests
JeanChristopheMorinPerso Jan 1, 2025
9609a36
More tests and docs
JeanChristopheMorinPerso Jan 1, 2025
22c2f6d
fix warning in docs
JeanChristopheMorinPerso Jan 1, 2025
1e32ddc
Fix handling of local wheels that would lead to duplicated packages
JeanChristopheMorinPerso Jan 2, 2025
8b0e22f
Fix PackageGroup.dists sharing a list. Default lists strike again!
JeanChristopheMorinPerso Jan 2, 2025
d883658
autouse setupPluginManager fixtures
JeanChristopheMorinPerso Jan 2, 2025
63418a4
Reset rez caches between tests
JeanChristopheMorinPerso Jan 2, 2025
2f0c2de
Add first integration test
JeanChristopheMorinPerso Jan 2, 2025
ff56b6a
Add missing .coveragerc
JeanChristopheMorinPerso Jan 2, 2025
ea54157
Try to fix integration tests on Windows
JeanChristopheMorinPerso Jan 2, 2025
6fe3e41
Remove hardcoded path separator in rez_pip.install.isWheelPure
JeanChristopheMorinPerso Jan 2, 2025
32dff3b
Fix integration tests again
JeanChristopheMorinPerso Jan 2, 2025
a3df115
Some more docs
JeanChristopheMorinPerso Jan 5, 2025
90e8b99
Fix order of hooks in docs
JeanChristopheMorinPerso Jan 5, 2025
d051a5e
Ignore /patches dir
JeanChristopheMorinPerso Jan 11, 2025
3e3bee0
Add .gitattributes
JeanChristopheMorinPerso Jan 11, 2025
ca8d3b9
Add a new patching mechanism by introducing a new patches hook
JeanChristopheMorinPerso Jan 11, 2025
7924a55
Add patches for PySide6
JeanChristopheMorinPerso Jan 11, 2025
3e8939d
Fix test_getHookImplementations and test_list_plugins
JeanChristopheMorinPerso Jan 11, 2025
996e0f2
Try to set TERM to dumb
JeanChristopheMorinPerso Jan 11, 2025
458b340
Fix rich output in CI and tests
JeanChristopheMorinPerso Jan 11, 2025
8817b02
Use fuzz=True when applying patches
JeanChristopheMorinPerso Jan 11, 2025
cece022
Add more PySide6 patches and don't apply patch with fuzzing
JeanChristopheMorinPerso Jan 12, 2025
55c2b62
Rework how the cleanup hook works. It now returns actions and rez-pip…
JeanChristopheMorinPerso Jan 12, 2025
107bcec
Some tying fixes
JeanChristopheMorinPerso Jan 12, 2025
57484c3
Add note about future breaking changes in the plugins doc
JeanChristopheMorinPerso Jan 12, 2025
5b76ca7
Adjust shiboken cleanup hook
JeanChristopheMorinPerso Jan 12, 2025
961d95e
Fix tests
JeanChristopheMorinPerso Jan 12, 2025
316f066
Improve docs and fix typos in docs
JeanChristopheMorinPerso Jan 12, 2025
9349997
Add some more logs to debug stuff
JeanChristopheMorinPerso Jan 12, 2025
ec51e08
Try to normalize path to posix
JeanChristopheMorinPerso Jan 12, 2025
887b1bd
Log patch_ng logs if applying a patch fails
JeanChristopheMorinPerso Jan 12, 2025
5bdd697
Remove superfluous logs
JeanChristopheMorinPerso Jan 12, 2025
97d4d08
Fix Windows console script test
JeanChristopheMorinPerso Jan 18, 2025
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
9 changes: 9 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[run]
branch = True
source_pkgs=rez_pip

[report]
exclude_also =
def __dir__
if TYPE_CHECKING:
if typing.TYPE_CHECKING:
5 changes: 5 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Set the default behavior, in case people don't have core.autocrlf set.
* text=auto

# Denote all files that are truly binary and should not be modified.
*.patch binary
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,4 @@ tests/data/rez_repo/
tests/data/_tmp_download/
docs/bin/
.idea/
/patches
27 changes: 27 additions & 0 deletions docs/source/api.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
===
API
===

.. warning:: The API is only meant to be used by plugins authors.

.. autoclass:: rez_pip.pip.T

.. autoclass:: rez_pip.pip.DownloadInfo
:members:

.. autoclass:: rez_pip.pip.Metadata
:members:
:undoc-members:

.. autoclass:: rez_pip.pip.PackageInfo
:members:

.. autoclass:: rez_pip.pip.PackageGroup
:members:
:show-inheritance:

.. autoclass:: rez_pip.pip.DownloadedArtifact
:members:

.. autoclass:: rez_pip.plugins.CleanupAction
:members:
183 changes: 182 additions & 1 deletion docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,20 @@
# https://www.sphinx-doc.org/en/master/usage/configuration.html

import re
import inspect
import argparse
import importlib

import docutils.nodes
import sphinx.transforms
import sphinx.util.nodes
import sphinx.application
import sphinx.ext.autodoc
import sphinx.util.docutils
import docutils.statemachine

import rez_pip.cli
import rez_pip.plugins

# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
Expand All @@ -25,6 +31,7 @@

extensions = [
# first-party extensions
"sphinx.ext.todo",
"sphinx.ext.autodoc",
"sphinx.ext.extlinks",
"sphinx.ext.intersphinx",
Expand Down Expand Up @@ -63,13 +70,26 @@
# https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html

intersphinx_mapping = {
"python": ("https://docs.python.org/3", None),
"rez": ("https://rez.readthedocs.io/en/stable/", None),
}

# Force usage of :external:
intersphinx_disabled_reftypes = ["*"]
# intersphinx_disabled_reftypes = ["*"]


# -- Options for sphinx.ext.autodoc ------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html

# autodoc_typehints = "description"
autodoc_typehints_format = "short"
autodoc_member_order = "bysource"

# -- Options for sphinx.ext.todo --------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/extensions/todo.html

todo_include_todos = True

# -- Custom ------------------------------------------------------------------
# Custom stuff

Expand Down Expand Up @@ -235,6 +255,167 @@ def run(self) -> list[docutils.nodes.Node]:
return node.children


class RezAutoPlugins(sphinx.util.docutils.SphinxDirective):
"""
Special rez-pip-autoplugins directive. This is quite similar to "autosummary" in some ways.
"""

required_arguments = 0
optional_arguments = 0

def run(self) -> list[docutils.nodes.Node]:
# Create the node.
node = docutils.nodes.section()
node.document = self.state.document

rst = docutils.statemachine.ViewList()

# Add rezconfig as a dependency to the current document. The document
# will be rebuilt if rezconfig changes.
self.env.note_dependency(rez_pip.plugins.__file__)
self.env.note_dependency(__file__)

path, lineNumber = self.get_source_info()

document = []
for plugin, hooks in rez_pip.plugins._getHookImplementations().items():
hooks = [f":func:`{hook}`" for hook in hooks]
document.append(f"* {plugin.split('.')[-1]}: {', '.join(hooks)}")

document = "\n".join(document)

# Add each line to the view list.
for index, line in enumerate(document.split("\n")):
# Note to future people that will look at this.
# "line" has to be a single line! It can't be a line like "this\nthat".
rst.append(line, path, lineNumber + index)

# Finally, convert the rst into the appropriate docutils/sphinx nodes.
sphinx.util.nodes.nested_parse_with_titles(self.state, rst, node)

# Return the generated nodes.
return node.children


class RezPipAutoPluginHooks(sphinx.util.docutils.SphinxDirective):
"""
Special rez-pip-autopluginhooks directive. This is quite similar to "autosummary" in some ways.
"""

required_arguments = 1
optional_arguments = 0

def run(self) -> list[docutils.nodes.Node]:
# Create the node.
node = docutils.nodes.section()
node.document = self.state.document

rst = docutils.statemachine.ViewList()

# Add rezconfig as a dependency to the current document. The document
# will be rebuilt if rezconfig changes.
self.env.note_dependency(rez_pip.plugins.__file__)
self.env.note_dependency(__file__)

path, lineNumber = self.get_source_info()

fullyQualifiedClassName = self.arguments[0]
module, klassname = fullyQualifiedClassName.rsplit(".", 1)

mod = importlib.import_module(module)
klass = getattr(mod, klassname)

methods = [
method
for method in inspect.getmembers(klass, predicate=inspect.isfunction)
if not method[0].startswith("_")
]

document = []
for method in sorted(methods, key=lambda x: x[1].__code__.co_firstlineno):
document.append(f".. autohook:: {module}.{klassname}.{method[0]}")

document = "\n".join(document)

# Add each line to the view list.
for index, line in enumerate(document.split("\n")):
# Note to future people that will look at this.
# "line" has to be a single line! It can't be a line like "this\nthat".
rst.append(line, path, lineNumber + index)

# Finally, convert the rst into the appropriate docutils/sphinx nodes.
sphinx.util.nodes.nested_parse_with_titles(self.state, rst, node)

# Return the generated nodes.
return node.children


def autodoc_process_signature(
app: sphinx.application.Sphinx,
what: str,
name: str,
obj,
options: dict,
signature: str,
return_annotation,
):
for name in ["Sequence", "Mapping", "MutableSequence"]:
signature = signature.replace(
f"rez_pip.compat.{name}", f"~collections.abc.{name}"
)
if return_annotation:
return_annotation = return_annotation.replace(
f"rez_pip.compat.{name}", f"~collections.abc.{name}"
)

signature = signature.replace(
"rez_pip.compat.importlib_metadata", "~importlib.metadata"
)

return signature, return_annotation


class HookDocumenter(sphinx.ext.autodoc.FunctionDocumenter):
"""
Custom autohook directive to document our hooks.
It allows us to easily document the hooks from the rez_pip.plugins.PluginSpec
class without exposing the class and module name.
"""

objtype = "hook" # auto + hook
directivetype = "function" # generated reST directive

def format_signature(self, **kwargs) -> str:
"""
Format the signature and remove self. We really don't want to expose
the class and module name or the fact that we are documenting methods.
"""
sig = super().format_signature(**kwargs)
sig = re.sub(r"\(self(,\s)?", "(", sig)

# Also force short names for our own types
sig = sig.replace("rez_pip.", "~rez_pip.")

return sig

def add_directive_header(self, sig):
modname = self.modname
# Hacky, but it does the job. This should remove the module name from the directive
# created by autodoc.
self.modname = ""

data = super().add_directive_header(sig)

# We need to restore it because autodoc does lots of things with the module name.
self.modname = modname
return data


def setup(app: sphinx.application.Sphinx):
app.add_directive("rez-autoargparse", RezAutoArgparseDirective)
app.add_directive("rez-pip-autoplugins", RezAutoPlugins)
app.add_directive("rez-pip-autopluginhooks", RezPipAutoPluginHooks)
app.add_transform(ReplaceGHRefs)

app.connect("autodoc-process-signature", autodoc_process_signature)
app.add_autodocumenter(HookDocumenter)
27 changes: 25 additions & 2 deletions docs/source/faq.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,31 @@ FAQ

List of commonly asked questions.

Why does the rez package created by rez-pip creates a variant per platform?
===========================================================================
Which packages does it support?
===============================

It technically supports all packages available on PyPI that are distributed as wheels.
Packages that only provide an sdist are not supported.

We say "technically" because there are some exceptions. Some packages on PyPI rely
on DSOs (shared libraries, i.e. ``.so``/``.DLL``/``.dylib`` files) that are not available on
all platforms. This is normal and is supported for most packages. However, there are some
packages that rely on methods like adding paths using :func:`os.add_dll_directory` or
hardcoded paths.

Some others rely on `path configuration (.pth) files <https://docs.python.org/3/library/site.html>`_.

When a package relies on these methods, rez-pip will successfully install it, but
the package might not function correctly (either partly or entirely).

The :doc:`plugin system <plugins>` was created to handled these cases. You can use plugins
to modify the package metatada, patch source files, add/remove files, etc.

``rez-pip`` comes with some :ref:`built-in plugins <plugins:built-in plugins>` for packages that are popular
in our communities and are known to be "broken" when installed with ``rez-pip``.

Why does the rez package created by rez-pip create a variant per platform?
==========================================================================

Sometimes rez-pip creates rez packages that have variants for the platform and arch on which they were installed,
and sometimes it even creates variants for Python versions. Bellow are the scenarios
Expand Down
3 changes: 3 additions & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Features
create per python version variants when installing a package that has console scripts.
* Better output logs.
* Implemented as an out-of-tree plugin, which means faster development cycle and more frequent releases.
* :doc:`Plugin system <plugins>` that allows for easy extensibility (experimental).
* Maintained by the rez maintainers.

Prerequisites
Expand Down Expand Up @@ -69,5 +70,7 @@ automatically created by the `install.py <https://github.com/AcademySoftwareFoun
command
transition
metadata
plugins
api
faq
changelog
71 changes: 71 additions & 0 deletions docs/source/plugins.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
=======
Plugins
=======

.. versionadded:: 0.4.0

.. warning::
Plugins are new and have not been tested througfully. There might be bugs, missing
features and rough edges.

We encourage you to try them out and report any issues you might find.

If you create your own plugins, expect them to break when you update
rez-pip's minor version. The plugin system is still in its early stages
and we will probably release breaking changes in the future.

rez-pip can be extended using plugins. Plugins can be used to do various things, such as
modifying packages (both metadata and files), etc.

This page documents the hooks available to plugins and how to implement plugins.

List installed plugins
======================

To list all installed plugins, use the :option:`rez-pip --list-plugins` command line argument.

Register a plugin
=================

rez-pip's plugin system is based on the `pluggy <https://pluggy.readthedocs.io/en/latest/>`_ framework,
and as such, plugins must be registered using `entry points <https://packaging.python.org/en/latest/specifications/entry-points/>`_.

The entry point group is named ``rez-pip``.

In a ``pyproject.toml`` file, it can be set like this:

.. code-block:: toml
:caption: pyproject.toml

[project.entry-points."rez-pip"]
my_plugin = "my_plugin_module"


Functions
=========

.. Not Using autodoc here because the decorator has a complex
signature to help type hinters. That signature is not needed
for the end user.
.. py:decorator:: rez_pip.plugins.hookimpl

Decorator used to register a plugin hook.

Hooks
=====

The list of available hooks is provided below. They are listed in the order they
are called by rez-pip.

.. rez-pip-autopluginhooks:: rez_pip.plugins.PluginSpec


Built-in plugins
================

rez-pip comes with some built-in plugins that are enabled by default. They exists mostly
to fix packages that are known to be "broken" if we don't fix them using plugins.

This lists the plugin names and the hooks they implement.

.. rez-pip-autoplugins::
Loading
Loading