preface

For anyone familiar with programming, function overrides are no stranger, but they are not universally applicable in all applications. As a simple example, when multiple project contributors want to participate in changes to the same program, frequent function rewrites can clutter up the code and make the entire project difficult to maintain.

So, is there a more elegant way to balance extensibility and stability in your code?

Yes, the authors of PyTest (the Python unit Testing framework) are aware of this problem. In its source code, you can find many functions decorated with the @pytest.hookimpl keyword, which means that the function is a plug-in implementation that is overridden by plug-in calls instead of functions.

Pytest pyTest

@pytest.hookimpl(hookwrapper=True)
def pytest_load_initial_conftests(early_config: Config):
	ns = early_config.known_args_namespace
	if ns.capture == "fd":
		_py36_windowsconsoleio_workaround(sys.stdout)
	_colorama_workaround()
	_readline_workaround()
	pluginmanager = early_config.pluginmanager
	capman = CaptureManager(ns.capture)
	pluginmanager.register(capman, "capturemanager")

	# make sure that capturemanager is properly reset at final shutdown
	early_config.add_cleanup(capman.stop_global_capturing)

	# finally trigger conftest loading but while capturing (issue93)
	capman.start_global_capturing()
	outcome = yield
	capman.suspend_global_capture()
	if outcome.excinfo is not None:
		out, err = capman.read_global_capture()
		sys.stdout.write(out)
		sys.stderr.write(err)
Copy the code

Earlier in PyTest this was just a plug-in library, but as the library grew, the authors separated it from PyTest and named it Pluggy.

What is pluggy?

Pluggy is the core component of PyTest for plug-in management and hook function calls. It has given more than 500 plug-ins the freedom to extend and customize pyTest’s default behavior. You could even say that PyTest itself is a collection of plug-ins that are called in turn according to a well-regulated protocol.

It enables users to extend or modify the behavior of the main program by installing plug-ins on it. Plug-in code runs as part of normal program execution to change or enhance some functionality of the program.

In essence, Pluggy “hooks” all functions so that users can easily build pluggable systems with it.

Why pluggy?

When multiple parties want to customize the extension, using function rewriting can lead to extremely confusing code. Pluggy allows loose coupling between the main program and the plug-in.

Pluggy also lets the main program designer think carefully about which objects to use in the hook function implementation. This provides a clear framework for plug-in authors because it shows how to extend the main program with a well-defined set of functions and objects, ultimately making the overall project code framework clearer and clearer.

How does Pluggy work?

First we need to understand two concepts:

  • Main program: a program that provides extensibility by specifying hook functions and calling their implementation during program execution;

  • Plug-in: a program implements the specified hook (part of it) and participates in program execution when the main program calls the implementation.

Pluggy, on the other hand, connects the main program to the plug-in by using the following aspects:

  • Define the hook specification corresponding to the call signature provided by the main program;
  • The hook implementation provided by the registered plug-in (also known as hookimpl- see callback);
  • Hook caller – an invocation loop that is triggered at the appropriate place in the main program to invoke the implementation and collect the results.

A simple example

To demonstrate pluggy’s core functionality, plug and play.

import pluggy

hookspec = pluggy.HookspecMarker("myproject")
hookimpl = pluggy.HookimplMarker("myproject")


class MySpec(object):
	"""A hook specification namespace."""

	@hookspec
	def myhook(self, arg1, arg2):
		"""My special little hook that you can customize."""


class Plugin_1(object):
	"""A hook implementation namespace."""

	@hookimpl
	def myhook(self, arg1, arg2):
		print("inside Plugin_1.myhook()")
		return arg1 + arg2


class Plugin_2(object):
	"""A 2nd hook implementation namespace."""

	@hookimpl
	def myhook(self, arg1, arg2):
		print("inside Plugin_2.myhook()")
		return arg1 - arg2


# create a manager and add the spec
pm = pluggy.PluginManager("myproject")
pm.add_hookspecs(MySpec)
# register plugins
pm.register(Plugin_1())
pm.register(Plugin_2())
# call our `myhook` hook
results = pm.hook.myhook(arg1=1, arg2=2)
print(results)
Copy the code

In the code above, the developer

  • Defines the main program to provide the call signature (myProject) corresponding to the hook specification – hookSpec;

  • Defines the hook implementation -hookimpl;

  • MySpec defines an input parameter to myHook.

  • Myhook implementations are defined in Plugin_1 and Plugin_2.

  • New plug-in management object PM;

  • Add the set hook specification MySpec to PM.

  • The defined Plugin_1 and Plugin_2 are registered in PM.

  • Myhook is called through PM.

The console output after running the code is as follows:

inside Plugin_2.myhook()
inside Plugin_1.myhook()
[-1, 3]
Copy the code

Why is the output [-1,3]? In fact, we can see that the two plug-ins implement the same function name. According to the principle of plug-in call, after calling myhook, the program will execute the plugin_2.myhook () first and then register the plugin_1.myhook () first. So in the final result, you get a list of return values in order of execution, with values -1 and 3.

At the end

After a period of study, I really understand the benefits of plug-in to the system in practice. It not only gives the program the possibility of infinite expansion, but also gives the code a stable infrastructure. This is important, because if the foundation is not strong enough, the program mansion will collapse at some point.

In reality, all programs are constantly evolving with the actual business. So there is never a one-size-fits-all design pattern, just a code structure that is constantly being optimized. When a coding method can not meet the status quo, meet the needs. There will always be someone to overturn it, to build a new model for the age. And that’s the beauty of programming.

If you think this article is helpful, you can click a like, and welcome to pay attention to my public number.