devel:newplugin => dev:plugin new, +dev:plugin install, +dev:plugin uninstall

This commit is contained in:
Gina Häußge 2015-10-30 10:10:47 +01:00
parent bc7b17b66f
commit ec491c3d0d
5 changed files with 224 additions and 73 deletions

View file

@ -6,12 +6,12 @@ octoprint.cli
.. automodule:: octoprint.cli
:members:
.. _sec-modules-cli-devel:
.. _sec-modules-cli-dev:
octoprint.cli.devel
-------------------
octoprint.cli.dev
-----------------
.. automodule:: octoprint.cli.devel
.. automodule:: octoprint.cli.dev
:members:
.. _sec-modules-cli-plugins:

View file

@ -10,6 +10,37 @@ Over the course of this little tutorial we'll build a full fledged, installable
at some locations throughout OctoPrint and also offers some other basic functionality to give you an idea of what
you can achieve with OctoPrint's plugin system.
First of all let use make sure that you have OctoPrint checked out and set up for development on your local
development environment::
$ cd ~/devel
$ git clone https://github.com/foosel/OctoPrint
[...]
$ cd OctoPrint
$ virtualenv venv
[...]
$ source venv/bin/activate
(venv) $ pip install -e[develop]
[...]
(venv) $ octoprint --help
Usage: octoprint [OPTIONS] COMMAND [ARGS]...
[...]
.. note::
You can also develop your plugin directly on your Raspberry Pi running OctoPi of course. In that
case please ignore the above instructions, you'll only need to activate the ``oprint``
virtual environment:
.. code-block:: none
$ source ~/oprint/bin/activate
(oprint) $ octoprint --help
Usage: octoprint [OPTIONS] COMMAND [ARGS]...
[...]
We'll start at the most basic form a plugin can take - just a couple of simple lines of Python code:
.. code-block:: python
@ -24,14 +55,15 @@ We'll start at the most basic form a plugin can take - just a couple of simple l
Saving this as ``helloworld.py`` in ``~/.octoprint/plugins`` yields you something resembling these log entries upon server startup::
(venv) $ octoprint serve
2015-01-27 11:14:35,124 - octoprint.server - INFO - Starting OctoPrint 1.2.0-dev-448-gd96e56e (devel branch)
[...]
2015-01-27 11:14:35,124 - octoprint.plugin.core - INFO - Loading plugins from /home/pi/.octoprint/plugins, /home/pi/OctoPrint/src/octoprint/plugins and installed plugin packages...
[...]
2015-01-27 11:14:36,135 - octoprint.plugin.core - INFO - 3 plugin(s) registered with the system:
| CuraEngine (bundled) = /home/pi/OctoPrint/src/octoprint/plugins/cura
| Discovery (bundled) = /home/pi/OctoPrint/src/octoprint/plugins/discovery
[...]
| Hello World (1.0.0) = /home/pi/.octoprint/plugins/helloworld.py
[...]
OctoPrint found that plugin in the folder and took a look into it. The name and the version it displays in that log
entry it got from the ``__plugin_name__`` and ``__plugin_version__`` lines. It also read the description from
@ -101,31 +133,35 @@ or alternatively manually utilizing Python's standard package manager ``pip`` di
So let's begin. We'll use the `cookiecutter <https://github.com/audreyr/cookiecutter>`_ template for OctoPrint plugins here,
so we'll first need to install that::
$ pip install cookiecutter
(venv) $ pip install cookiecutter
Then we can use the ``cookiecutter`` command to generate a new OctoPrint plugin skeleton for us::
Then we can use the ``octoprint dev:plugin new`` command [#f1]_ to generate a new OctoPrint plugin skeleton for us::
$ cookiecutter gh:OctoPrint/cookiecutter-octoprint-plugin
(venv) $ cd ~/devel
(venv) $ octoprint dev:plugin new helloworld
Cloning into 'cookiecutter-octoprint-plugin'...
remote: Counting objects: 70, done.
remote: Compressing objects: 100% (17/17), done.
emote: Total 70 (delta 0), reused 0 (delta 0), pack-reused 51
Unpacking objects: 100% (70/70), done.
Checking connectivity... done.
plugin_identifier (default is "skeleton")? helloworld
plugin_package (default is "octoprint_helloworld")?
plugin_name (default is "OctoPrint-Helloworld")?
repo_name (default is "OctoPrint-Helloworld")?
full_name (default is "You")? Your Name
email (default is "you@example.com")? you@somewhere.net
github_username (default is "you")? yourGithubName
plugin_version (default is "0.1.0")? 1.0.0
plugin_description (default is "TODO")? A quick "Hello World" example plugin for OctoPrint
plugin_license (default is "AGPLv3")?
plugin_homepage (default is "https://github.com/yourGithubName/OctoPrint-Helloworld")?
plugin_source (default is "https://github.com/yourGithubName/OctoPrint-Helloworld")?
plugin_installurl (default is "https://github.com/yourGithubName/OctoPrint-Helloworld/archive/master.zip")?
$ cd OctoPrint-HelloWorld
remote: Counting objects: 101, done.
remote: Total 101 (delta 0), reused 0 (delta 0), pack-reused 101
Receiving objects: 100% (101/101), 53.69 KiB, done.
Resolving deltas: 100% (35/35), done.
plugin_package [octoprint_helloworld]:
plugin_name [OctoPrint-Helloworld]:
repo_name [OctoPrint-Helloworld]:
full_name [You]: Your Name
email [you@example.com]: you@somewhere.net
github_username [you]: yourGithubName
plugin_version [0.1.0]: 1.0.0
plugin_description [TODO]: A quick "Hello World" example plugin for OCtoPrint
plugin_license [AGPLv3]:
plugin_homepage [https://github.com/yourGithubName/OctoPrint-Helloworld]:
plugin_source [https://github.com/yourGithubName/OctoPrint-Helloworld]:
plugin_installurl [https://github.com/yourGithubName/OctoPrint-Helloworld/archive/master.zip]:
(venv) $ cd OctoPrint-HelloWorld
.. note::
If ``octoprint dev:plugin new`` isn't recognized as a command (and also doesn't show up in the output of
``octoprint --help``, make sure you installed cookiecutter into the same python environment as OctoPrint.
This will create a project structure in the ``OctoPrint-HelloWorld`` folder we just changed to that looks like this::
@ -193,9 +229,11 @@ Now all that's left to do is to move our ``helloworld.py`` into the ``octoprint_
The plugin is now ready to be installed via ``python setup.py install``. However, since we are still
working on our plugin, it makes more sense to use ``python setup.py develop`` for now -- this way the plugin becomes
discoverable by OctoPrint, however we don't have to reinstall it after any changes we will still do::
discoverable by OctoPrint, however we don't have to reinstall it after any changes we will still do. We can have the
``octoprint dev:plugin install`` command do everything for us here, it will ensure to use the python binary belonging
to your OctoPrint installation::
$ python setup.py develop
(venv) $ octoprint dev:plugin install
running develop
running egg_info
creating OctoPrint_HelloWorld.egg-info
@ -209,9 +247,8 @@ Restart OctoPrint. Your plugin should still be properly discovered and the log l
2015-01-27 13:43:34,134 - octoprint.plugin.core - INFO - Loading plugins from /home/pi/.octoprint/plugins, /home/pi/OctoPrint/src/octoprint/plugins and installed plugin packages...
[...]
2015-01-27 13:43:34,818 - octoprint.plugin.core - INFO - 3 plugin(s) registered with the system:
| CuraEngine (bundled) = /home/pi/OctoPrint/src/octoprint/plugins/cura
| Discovery (bundled) = /home/pi/OctoPrint/src/octoprint/plugins/discovery
| Hello World (1.0.0) = /home/pi/OctoPrint-HelloWorld/octoprint_helloworld
[...]
| Hello World (1.0.0) = /home/pi/devel/OctoPrint-HelloWorld/octoprint_helloworld
[...]
2015-01-27 13:43:38,997 - octoprint.plugins.helloworld - INFO - Hello World!
@ -257,9 +294,9 @@ and ``__plugin_description__`` from ``__init__.py``:
and restart OctoPrint::
2015-01-27 13:46:33,786 - octoprint.plugin.core - INFO - 3 plugin(s) registered with the system:
| CuraEngine (bundled) = /home/pi/OctoPrint/src/octoprint/plugins/cura
| Discovery (bundled) = /home/pi/OctoPrint/src/octoprint/plugins/discovery
| OctoPrint-HelloWorld (1.0.0) = /home/pi/OctoPrint-HelloWorld/octoprint_helloworld
[...]
| OctoPrint-HelloWorld (1.0.0) = /home/pi/devel/OctoPrint-HelloWorld/octoprint_helloworld
[...]
Our "Hello World" Plugin still gets detected fine, but it's now listed under the same name it's installed under,
"OctoPrint-HelloWorld". That's a bit redundant and squashed, so we'll override that bit via ``__plugin_name__`` again:
@ -284,9 +321,9 @@ Our "Hello World" Plugin still gets detected fine, but it's now listed under the
Restart OctoPrint again::
2015-01-27 13:48:54,122 - octoprint.plugin.core - INFO - 3 plugin(s) registered with the system:
| CuraEngine (bundled) = /home/pi/OctoPrint/src/octoprint/plugins/cura
| Discovery (bundled) = /home/pi/OctoPrint/src/octoprint/plugins/discovery
[...]
| Hello World (1.0.0) = /home/pi/OctoPrint-HelloWorld/octoprint_helloworld
[...]
Much better! You can override pretty much all of the metadata defined within ``setup.py`` from within your Plugin itself --
take a look at :ref:`the available control properties <sec-plugin-concepts-controlproperties>` for all available
@ -295,7 +332,7 @@ overrides.
Following the README of the `Plugin Skeleton <https://github.com/OctoPrint/OctoPrint-PluginSkeleton>`_ you could now
already publish your plugin on Github and it would be directly installable by others using pip::
pip install https://github.com/yourGithubName/OctoPrint-HelloWorld/archive/master.zip
(venv) $ pip install https://github.com/yourGithubName/OctoPrint-HelloWorld/archive/master.zip
But let's add some more features instead.
@ -857,7 +894,7 @@ generated CSS files (and compiles them on the fly in your browser using `lessjs
which makes development so much easier. Let's try that, so you know how it works for future bigger projects.
Add another folder to our ``static`` folder called ``less`` and within that create a file ``helloworld.less``. Put
into that the same content as into our CSS file. Compile that LESS file to CSS [#f1]_, overwriting our old ``helloworld.css``
into that the same content as into our CSS file. Compile that LESS file to CSS [#f2]_, overwriting our old ``helloworld.css``
in the process. The folder structure of our plugin should now look like this::
octoprint_helloworld/
@ -1038,6 +1075,12 @@ looking for examples.
.. rubric:: Footnotes
.. [#f1] Refer to the `LESS documentation <http://lesscss.org/#using-less>`_ on how to do that. If you are developing
.. [#f1] Instead of the ``octoprint dev:plugin new`` you could also have manually called cookiecutter with the
template's repository URL shortcut: ``cookiecutter gh:OctoPrint/cookiecutter-octoprint-plugin``. The
``devel:newplugin`` command already does this for you, makes sure cookiecutter always uses a fresh
checkout without prompting you for it and also allows to pre-specify a bunch of settings (like the
plugin's identifier) directly from the command line. Take a look at ``octoprint dev:plugin new --help``
for the usage details.
.. [#f2] Refer to the `LESS documentation <http://lesscss.org/#using-less>`_ on how to do that. If you are developing
your plugin under Windows you might also want to give `WinLESS <http://winless.org/>`_ a look which will run
in the background and keep your CSS files up to date with your various project's LESS files automatically.

View file

@ -118,10 +118,10 @@ legacy_options = bulk_options([
from .server import server_commands
from .plugins import plugin_commands
from .devel import devel_commands
from .dev import dev_commands
@click.group(name="octoprint", invoke_without_command=True, cls=click.CommandCollection,
sources=[server_commands, plugin_commands, devel_commands])
sources=[server_commands, plugin_commands, dev_commands])
@standard_options()
@legacy_options
@click.version_option(version=octoprint.__version__)

View file

@ -16,8 +16,53 @@ class OctoPrintDevelCommands(click.MultiCommand):
based on availability of development dependencies.
"""
prefix = "dev"
sep = ":"
def __init__(self, *args, **kwargs):
click.MultiCommand.__init__(self, *args, **kwargs)
from octoprint.util.commandline import CommandlineCaller
from octoprint.util.pip import LocalPipCaller
from functools import partial
def log_util(f):
def log(*lines):
for line in lines:
f(line)
return log
log_call = log_util(lambda x: click.echo(">> {}".format(x)))
log_stdout = log_util(click.echo)
log_stderr = log_util(partial(click.echo, err=True))
self.pip_caller = LocalPipCaller()
self.pip_caller.on_log_call = log_call
self.pip_caller.on_log_stdout = log_stdout
self.pip_caller.on_log_stderr = log_stderr
self.command_caller = CommandlineCaller()
self.command_caller.on_log_call = log_call
self.command_caller.on_log_stdout = log_stdout
self.command_caller.on_log_stderr = log_stderr
def _get_prefix_methods(self, method_prefix):
for name in [x for x in dir(self) if x.startswith(method_prefix)]:
method = getattr(self, name)
yield method
def _get_commands_from_prefix_methods(self, method_prefix):
for method in self._get_prefix_methods(method_prefix):
result = method()
if result is not None and isinstance(result, click.Command):
yield result
def _get_commands(self):
commands = dict()
for command in self._get_commands_from_prefix_methods("group_"):
commands[self.prefix + self.sep + command.name] = command
return commands
def list_commands(self, ctx):
result = [name for name in self._get_commands()]
result.sort()
@ -27,22 +72,14 @@ class OctoPrintDevelCommands(click.MultiCommand):
commands = self._get_commands()
return commands.get(cmd_name, None)
def _get_commands(self):
commands = dict()
def group_plugin(self):
command_group = click.Group("plugin",
help="Helpers for plugin developers")
for command in self._get_commands_from_prefix_methods("plugin_"):
command_group.add_command(command)
return command_group
for name in [x for x in dir(self) if x.startswith("command_")]:
method = getattr(self, name)
try:
result = method()
if result is not None:
commands["devel" + self.sep + result.name] = result
except:
logging.getLogger(__name__).exception("There was an error registering one of the devel commands ({})".format(name))
return commands
def command_newplugin(self):
def plugin_new(self):
try:
import cookiecutter.main
except ImportError:
@ -102,7 +139,7 @@ class OctoPrintDevelCommands(click.MultiCommand):
finally:
cookiecutter.main.prompt_for_config = original_prompt_for_config
@click.command("newplugin")
@click.command("new")
@click.option("--name", "-n", help="The name of the plugin")
@click.option("--package", "-p", help="The plugin package")
@click.option("--author", "-a", help="The plugin author's name")
@ -153,6 +190,54 @@ class OctoPrintDevelCommands(click.MultiCommand):
return command
def plugin_install(self):
@click.command("install")
@click.option("--path", help="Path of the local plugin development folder to install")
def command(path):
"""
Installs the local plugin in development mode.
Note: This can NOT be used to install plugins from remote locations
such as the plugin repository! It is strictly for local development
of plugins, to ensure the plugin is installed (editable) into the
same python environment that OctoPrint is installed under.
"""
import os
import sys
if not path:
path = os.getcwd()
# check if this really looks like a plugin
if not os.path.isfile(os.path.join(path, "setup.py")):
click.echo("This doesn't look like an OctoPrint plugin folder")
sys.exit(1)
self.command_caller.call([sys.executable, "setup.py", "develop"], cwd=path)
return command
def plugin_uninstall(self):
if not self.pip_caller.available:
return
@click.command("uninstall")
@click.argument("name")
def command(name):
"""Uninstalls the plugin with the given name."""
import sys
lower_name = name.lower()
if not lower_name.startswith("octoprint_") and not lower_name.startswith("octoprint-"):
click.echo("This doesn't look like an OctoPrint plugin name")
sys.exit(1)
call = ["uninstall", "--yes", name]
self.pip_caller.execute(*call)
return command
@click.group(cls=OctoPrintDevelCommands)
def devel_commands():
def dev_commands():
pass

View file

@ -114,7 +114,7 @@ class PipCaller(CommandlineCaller):
self._version = None
self.refresh = False
def execute(self, *args):
def execute(self, *args, **kwargs):
if self.refresh:
self.trigger_refresh()
@ -151,20 +151,10 @@ class PipCaller(CommandlineCaller):
if self._use_sudo or self.force_sudo:
command = ["sudo"] + command
return self.call(command)
return self.call(command, **kwargs)
def _setup_pip(self):
pip_command = self.configured
if pip_command is not None and pip_command.startswith("sudo "):
pip_command = pip_command[len("sudo "):]
pip_sudo = True
else:
pip_sudo = False
if pip_command is None:
pip_command = self._autodetect_pip()
pip_command, pip_sudo = self._get_pip_command()
if pip_command is None:
return
@ -211,6 +201,20 @@ class PipCaller(CommandlineCaller):
self._virtual_env = pip_virtual_env
self._install_dir = pip_install_dir
def _get_pip_command(self):
pip_command = self.configured
if pip_command is not None and pip_command.startswith("sudo "):
pip_command = pip_command[len("sudo "):]
pip_sudo = True
else:
pip_sudo = False
if pip_command is None:
pip_command = self._autodetect_pip()
return pip_command, pip_sudo
def _autodetect_pip(self):
import os
python_command = sys.executable
@ -325,3 +329,22 @@ class PipCaller(CommandlineCaller):
else:
self._logger.debug("Could not detect desired output from testballoon install, got this instead: {}".format(" ".join(sarge_command), output))
return False, False, False, None
class LocalPipCaller(PipCaller):
def _get_pip_command(self):
return self._autodetect_pip(), False
def _check_pip_setup(self, pip_command):
import sys
import os
from distutils.sysconfig import get_python_lib
virtual_env = hasattr(sys, "real_prefix")
install_dir = get_python_lib()
writable = os.access(install_dir, os.W_OK)
return writable or not virtual_env, \
not writable and not virtual_env and site.ENABLE_USER_SITE, \
virtual_env, \
install_dir