Skip to content

Commit

Permalink
Initial CLI. (#36)
Browse files Browse the repository at this point in the history
* Removes runtime dependency on psutil. (Fixes #30)
* Reduce unnecessary imports and import-time work.
* Fix test issue caused by not flushing intercepted std streams.
* Partially implements #19 and #32.
  • Loading branch information
chrahunt authored May 4, 2019
1 parent e068424 commit 3180949
Show file tree
Hide file tree
Showing 50 changed files with 1,729 additions and 510 deletions.
136 changes: 110 additions & 26 deletions docs/index.rst
Original file line number Diff line number Diff line change
@@ -1,57 +1,141 @@
quicken
===================================
=======

Quicken is a Python package that enables CLI-based tools to start more quickly.
Quicken is a library that helps Python applications start more quickly, with a focus on doing the "right thing" for
wrapping CLI-based tools.

When added to a command-line tool, Quicken starts a server transparently the first time the tool is invoked.
After the server is running, it is responsible for executing commands. This is fast because imports happen once at
server start.
When a quicken-wrapped command is executed the first time, an application server will be started. If the server
is already up then the command will be executed in a ``fork``ed child, which avoids the overhead of loading
libraries.
The library is transparent for users. Every time a command is run all context is sent to the server, including:
Quicken only speeds up applications on Unix platforms, but falls back to executing commands directly
on non-Unix platforms.
* arguments
The library tries to be transparent for applications. Every time a command is run all context is sent from the
client to the server, including:
* command-line arguments
* current working directory
* environment
* umask
* file descriptors for stdin/stdout/stderr
Usage:

Assume your application is ``my_app`` and your original CLI entrypoint is ``my_app.cli.cli``. Create a file ``my_app/cli_wrapper.py``, with contents:

.. code-block:: python
from quicken import cli_factory
``quicken.script``
==================

For command-line tool authors that want to speed up applications, simply add quicken as a
dependency, then use ``quicken.script`` in your ``console_scripts`` (or equivalent).

@cli_factory('my_app')
def main():
# Import your existing command-line entrypoint.
# This is the expensive operation that only happens once.
from .cli import cli
# Return it.
return cli
If your existing entry point is ``my_app.cli:main``, then you would use ``quicken.script:my_app.cli._.main``.

Adapt ``setup.py``:
For example, if using setuptools (``setup.py``):

.. code-block:: python
setup(
...
entry_points={
'console_scripts': ['my-command=my_app.cli_wrapper:cli']
'console_scripts': [
'my-command=my_app.cli:main',
# With quicken
'my-commandc=quicken.script:my_app.cli._.main',
],
},
...
)
If you have ``my_app/__main__.py``, it should look like:
If using poetry

.. code-block:: toml
[tools.poetry.scripts]
poetry = 'poetry:console.run'
# With quicken
poetryc = 'quicken.script:poetry._.console.run'
If using flit

.. code-block:: toml
[tools.flit.scripts]
flit = "flit:main"
# With quicken
flitc = "quicken.script:flit._.main"
``quicken`` command
===================

The ``quicken`` command can be used with basic scripts and command-line tools that do not use quicken built-in.

Given a script ``script.py``, like

.. code-block:: python
from .cli_wrapper import main
import click
import requests
...
@click.command()
def main():
"""My script."""
...
if __name__ == '__main__':
main()
running ``quicken -f script.py arg1 arg2`` once will start the application server then run ``main()``. Running the command
again like ``quicken -f script.py arg2 arg3`` will run the command on the server, and should be faster

If ``script.py`` is changed then the server will be restarted the next time the command is run.


Differences and restrictions
============================

The library tries to be transparent for applications, but it cannot be exactly the same. Specifically here
is the behavior you can expect:

* ``sys.argv`` is set to the list of arguments of the client
* ``sys.stdin``, ``sys.stdout``, and ``sys.stderr`` are sent from the client to the command process, any console loggers
are re-initialized with the new streams
* ``os.environ`` is copied from the client to the command process
* we change directory to the cwd of the client process

The setup above is guaranteed to be done before the registered script function
is invoked.

In addition:

* signals sent to the client that can be forwarded are sent to the command process
* for SIGTSTP (C-z at a terminal) and SIGTTIN, we send SIGSTOP to the command process and then stop the client process
* for SIGKILL, which cannot be intercepted, the server recognizes when the connection to the client is broken and will
kill the command process soon after
* when the command runner exits, the client exits with the same exit code
* if a client and the server differ in group id or supplementary group ids then a new
server process is started before the command is run

While the registered script function is executed in a command process, the initial import of
the module is done by the first client that is executed. For that reason, there are several things that
should be avoided outside of the registered script function:

1. starting threads - because we fork to create the command runner process, it may cause undesirable effects if
threads are created
2. read configuration based on environment, arguments, or current working directory - if done when a module is imported
then it will capture the values of the client that started the server
3. set signal handlers - this will only be setting signal handlers for the first client starting
the server and these are overridden to forward signals to the command runner process
4. start sub-processes
5. reading plugin information (from e.g. ``pkg_resources``) - this will only be at server start time,
and not when the command is actually run

Currently the following is unsupported at any point:

* ``atexit`` handlers - they will not be run at the end of the handler process
* ``setuid`` or ``setgid`` are currently unsupported

main()

.. toctree::
:maxdepth: 2
Expand Down
3 changes: 2 additions & 1 deletion docs/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ certifi==2019.3.9
ch==0.1.2
chardet==3.0.4
coverage==4.5.3
demandimport==0.3.4
docutils==0.14
fasteners==0.14.1
idna==2.8
Expand All @@ -23,7 +24,7 @@ packaging==19.0
pathtools==0.1.2
pid==2.2.3
pluggy==0.9.0
psutil==5.6.1
psutil==5.6.2
py==1.8.0
py-cpuinfo==5.0.0
pydevd==1.6.0
Expand Down
4 changes: 4 additions & 0 deletions examples/slow_start/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,7 @@ def cli():
print('cli()')
print('cli2()')
logger.info('cli()')


if __name__ == '__main__':
cli()
8 changes: 4 additions & 4 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ repository = "https://github.com/chrahunt/quicken"
[tool.poetry.dependencies]
python = "^3.7"
pid = "^2.2"
psutil = "^5.4"
tblib = "^1.3"
fasteners = "^0.14.1"

Expand All @@ -32,6 +31,11 @@ sphinxcontrib-trio = "^1.0"
ch = "^0.1.2"
pytest-benchmark = "^3.2"

[tool.poetry.scripts]
quicken = "quicken._cli:main"
quicken-c = "quicken.script:quicken._cli._.main"
quicken-ctl = "quicken.ctl_script:quicken._cli._.main"

[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"
10 changes: 4 additions & 6 deletions quicken/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
from ._decorator import quicken

"""
"""
# Top-level package should be free from any unnecessary imports, since they are
# unconditional.
__version__ = '0.1.0'


class QuickenError(Exception):
pass
5 changes: 5 additions & 0 deletions quicken/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from ._cli import main


if __name__ == '__main__':
main()
Loading

0 comments on commit 3180949

Please sign in to comment.