Skip to content

Commit

Permalink
[docs] prepare documentation for release
Browse files Browse the repository at this point in the history
- Add ability to autosummary all top-level packages dynamically.
    - Autopopulate rst_epilog to support this (and any future
      moves such as ci_exec.core.rm_rf => ci_exec.commands.rm_rf),
      top-level get |replacements| (e.g., |rm_rf|).
    - Update most (if not, all) top level links to use |replacements|.
- Add brief and long descriptions on README, share them on index.
  • Loading branch information
svenevs committed May 14, 2019
1 parent bea5131 commit c262902
Show file tree
Hide file tree
Showing 13 changed files with 301 additions and 68 deletions.
52 changes: 51 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,54 @@ ci_exec

.. end_badges
Minimal python wrapper designed for running CI build steps via Python.
.. begin_brief_desc
Minimal python wrapper designed for running continuous integration (CI) build steps
using Python.

.. end_brief_desc
.. begin_long_desc
Managing cross platform build scripts for CI can become tedious at times when you need
to e.g., maintain two nearly identical scripts ``install_deps.sh`` and
``install_deps.bat`` due to incompatible syntaxes. ``ci_exec`` enables a single file
to manage this using Python 3.5+.

The ``ci_exec`` package provides a set of wrappers / utility functions designed
specifically for running build steps on CI providers. It is

**Logging by Default**
Commands executed, including their full command-line arguments, are logged. This
includes any output on ``stdout`` / ``stderr`` from the commands. The logging
resembles what ``set -x`` would give you in a shell script. For commands that will
take a long time, as long as output is being produced, this will additionally
prevent timeouts on the build.

**Failing by Default**
Any command that does not succeed will fail the entire build. An attempt to exit
with the same exit code as the command that failed will be performed. Meaning the
CI provider will correctly report a failed build.

**Convenient**
``ci_exec`` affords users the ability to write shell-like scripts that will work
on any platform that Python can run on. A simple example::

from ci_exec import cd, which

cmake = which("cmake")
ninja = which("ninja")
with cd("build", create=True):
cmake("..", "-G", "Ninja", "-DCMAKE_BUILD_TYPE=Release")
ninja("-j", "2", "test")

.. end_long_desc
Please refer to the `full documentation <https://ci-exec.readthedocs.io/en/latest/>`_
for more information.

License
========================================================================================

This software is licensed under the Apache 2.0 license.

10 changes: 5 additions & 5 deletions ci_exec/colorize.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ def colorize(message: str, *, color: str, style: str = Styles.Regular) -> str:


def dump_predefined_color_styles():
"""Dump all predefined :class:`Colors` in every :class:`Styles` to the console."""
"""Dump all predefined |Colors| in every |Styles| to the console."""
for c_key in Colors.__dict__.keys():
if not c_key[0].isupper():
continue
Expand Down Expand Up @@ -241,12 +241,12 @@ def log_stage(stage: str, *, fill_char: str = "=", pad: str = " ",
-----------------------------+ CMake.Configure -----------------------------
color : str or None
The ANSI color code to use with :func:`colorize`. If no coloring is desired,
call this function with ``color=None`` to disable.
The ANSI color code to use with |colorize|. If no coloring is desired, call
this function with ``color=None`` to disable.
style : str
The ANSI style specification to use with :func:`colorize`. If no coloring is
desired, leave this parameter as is and specify ``color=None``.
The ANSI style specification to use with |colorize|. If no coloring is desired,
leave this parameter as is and specify ``color=None``.
width : int
If specified, the terminal size will be ignored and a message formatted to this
Expand Down
61 changes: 27 additions & 34 deletions ci_exec/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class Executable:
Each executable is:
1. Failing by default: unless called with ``check=False``, any execution
that fails (has a non-zero exit code) will result in a call to :func:`fail`,
that fails (has a non-zero exit code) will result in a call to |fail|,
terminating the entire application.
2. Logging by default: every call executed will print what will be run in color
and then dump the output of the command. In the event of failure, this makes
Expand All @@ -76,7 +76,7 @@ class Executable:
.. code-block:: console
> python lala.py
> python simple.py
$ /usr/bin/git remote
origin
$ /usr/bin/git log -1 --petty=%B
Expand All @@ -86,12 +86,12 @@ class Executable:
> echo $?
128
See :func:`__call__` for more information on how this works.
See :func:`__call__` for more information.
.. tip::
Hard-coded paths in these examples were for demonstrative purposes. In practice
this should not be done, use :func:`which` instead.
this should not be done, use |which| instead.
Attributes
----------
Expand All @@ -108,16 +108,14 @@ class Executable:
``""`` to have no prefix.
log_color : str
The ``color`` code to use when calling :func:`~ci_exec.colorize.colorize` to
display the next invocation of :func:`__call__`. Set to ``None`` to disable
colorizing each log of :func:`__call__`. Default:
:data:`Colors.Cyan <ci_exec.colorize.Colors.Cyan>`.
The ``color`` code to use when calling |colorize| to display the next invocation
of :func:`__call__`. Set to ``None`` to disable colorizing each log of
:func:`__call__`. Default: :data:`Colors.Cyan <ci_exec.colorize.Colors.Cyan>`.
log_style : str
The ``style`` code to use when calling :func:`~ci_exec.colorize.colorize` to
display the next invocation of :func:`__call__`. If no colors are desired, set
``log_color`` to ``None``. Default:
:data:`Styles.Bold <ci_exec.colorize.Styles.Bold>`.
The ``style`` code to use when calling |colorize| to display the next invocation
of :func:`__call__`. If no colors are desired, set ``log_color`` to ``None``.
Default: :data:`Styles.Bold <ci_exec.colorize.Styles.Bold>`.
Raises
------
Expand Down Expand Up @@ -174,7 +172,7 @@ def __init__(self, exe_path: str, *, log_calls: bool = True,
self.log_color = log_color
self.log_style = log_style

def __call__(self, *args, **kwargs):
def __call__(self, *args, **kwargs) -> subprocess.CompletedProcess:
"""
Run :attr:`exe_path` with the specified command-line ``*args``.
Expand All @@ -194,7 +192,7 @@ def __call__(self, *args, **kwargs):
.. warning::
**Any** exceptions generated result in a call to :func:`fail`, which will
**Any** exceptions generated result in a call to |fail|, which will
terminate the application.
Parameters
Expand All @@ -206,8 +204,8 @@ def __call__(self, *args, **kwargs):
**kwargs
The key-value arguments are all forwarded to :func:`python:subprocess.run`.
If ``check`` is not provided, this is an implicit ``check=True``. That is,
if you do **not** want the application to exit (via :func:`fail`), you
**must** specify ``check=False``:
if you do **not** want the application to exit (via |fail|), you **must**
specify ``check=False``:
.. code-block:: python
Expand Down Expand Up @@ -267,9 +265,11 @@ def __call__(self, *args, **kwargs):
# is executing.
try:
# NOTE: 3.5 has no ending period, 3.6+ do.
exit_code = int(
re.match(r".*non-zero exit status (\d+)\.?", err_msg).group(1)
)
match = re.match(r".*non-zero exit status (\d+)\.?", err_msg)
if match:
exit_code = int(match.group(1))
else:
exit_code = 1
except: # noqa: E722
exit_code = 1
fail(err_msg, exit_code=exit_code)
Expand Down Expand Up @@ -306,7 +306,7 @@ def mkdir_p(path: Union[Path, str], mode: int = 0o777, parents: bool = True,
If the path exists and is a directory with ``exist_ok=True``, the command
will succeed. If the path exists and is a **file**, even with
``exist_ok=True`` the command will :func:`fail`.
``exist_ok=True`` the command will |fail|.
"""
if isinstance(path, str):
path = Path(path)
Expand All @@ -318,19 +318,15 @@ def mkdir_p(path: Union[Path, str], mode: int = 0o777, parents: bool = True,

def rm_rf(path: Union[Path, str], ignore_errors: bool = False, onerror=None):
"""
Permissive wrapper around :func:`python:shutil.rmtree` bypassing |gotchas|.
.. |gotchas| replace::
:class:`python:FileNotFoundError` and :class:`python:NotADirectoryError`
Permissive wrapper around :func:`python:shutil.rmtree` bypassing :class:`python:FileNotFoundError` and :class:`python:NotADirectoryError`.
This function simply checks if ``path`` exists first before calling
:func:`python:shutil.rmtree`. If the ``path`` does not exist, nothing is done. If
the path exists but is a file, :meth:`python:pathlib.Path.unlink` is called instead.
Essentially, this function tries to behave like ``rm -rf``, but in the event that
removal is not possible (e.g., due to insufficient permissions), the function will
still :func:`fail`.
still |fail|.
Parameters
----------
Expand All @@ -343,7 +339,7 @@ def rm_rf(path: Union[Path, str], ignore_errors: bool = False, onerror=None):
onerror
See :func:`python:shutil.rmtree` for more information on the callback.
"""
""" # noqa: E501
if isinstance(path, str):
path = Path(path)
if not path.exists():
Expand All @@ -360,13 +356,11 @@ def rm_rf(path: Union[Path, str], ignore_errors: bool = False, onerror=None):
def which(cmd: str, *, mode: int = (os.F_OK | os.X_OK), path: Optional[str] = None,
**kwargs) -> Executable:
"""
Restrictive wrapper around |shutil_which| that will :func:`fail` if not found.
.. |shutil_which| replace:: :func:`python:shutil.which`
Restrictive wrapper around :func:`shutil.which` that will |fail| if not found.
The primary difference is that when ``cmd`` is not found,
:func:`python:shutil.which` will return ``None`` whereas this function will
:func:`fail`. If you need to conditionally check for a command, do **not** use this
|fail|. If you need to conditionally check for a command, do **not** use this
function, use :func:`python:shutil.which` instead.
Parameters
Expand All @@ -382,9 +376,8 @@ def which(cmd: str, *, mode: int = (os.F_OK | os.X_OK), path: Optional[str] = No
Default: ``None``. See :func:`python:shutil.which`.
**kwargs
Included as a convenience bypass, forwards directly to :class:`Executable`
constructor. Suppose a non-logging :class:`Executable` is desired. One
option::
Included as a convenience bypass, forwards directly to |Executable| constructor.
Suppose a non-logging |Executable| is desired. One option::
git = which("git")
git.log_calls = False
Expand Down
13 changes: 6 additions & 7 deletions ci_exec/patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ def filter_file(path: Union[Path, str], pattern: str,
copying e.g., ``file.txt`` to ``file.txt.orig``.
2. Perform filtering using :func:`python:re.sub`.
3. If ``demand_different=True`` (default), verify that replacements were actually
made. If not, :func:`~ci_exec.core.fail`.
made. If not, |fail|.
The only required arguments are ``path``, ``pattern``, and ``repl``. If any errors
occur, including invalid input, this function will :func:`~ci_exec.core.fail`.
occur, including invalid input, this function will |fail|.
.. |pass_through| replace:: Pass-through parameter to :func:`python:re.sub`.
Expand Down Expand Up @@ -73,9 +73,8 @@ def filter_file(path: Union[Path, str], pattern: str,
regular expressions depending on the replacement needed.
demand_different : bool
Whether or not this function should :func:`~ci_exec.core.fail` if no changes
were actually made. Default: ``True``, :func:`~ci_exec.core.fail` if no
filtering was performed.
Whether or not this function should |fail| if no changes were actually made.
Default: ``True``, |fail| if no filtering was performed.
encoding : str or None
The encoding to open files with. Default: ``None`` implies default.
Expand Down Expand Up @@ -143,8 +142,8 @@ def unified_diff(from_path: Union[Path, str], to_path: Union[Path, str], n: int
r"""
Return the :func:`unified_diff <difflib.unified_diff>` between two files.
Any errors, such as not being able to read a file, will :func:`~ci_exec.core.fail`
the application abruptly.
Any errors, such as not being able to read a file, will |fail| the application
abruptly.
Parameters
----------
Expand Down
4 changes: 2 additions & 2 deletions ci_exec/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def provider(func: Callable) -> staticmethod:
"""
Mark a function as a CI provider.
**Not intended for use outside of the** :class:`Provider` **class**.
**Not intended for use outside of the** |Provider| **class**.
Parameters
----------
Expand All @@ -49,7 +49,7 @@ def provider(func: Callable) -> staticmethod:

class ProviderMeta(type):
"""
Metaclass for :class:`Provider`.
Metaclass for |Provider|.
This metaclass populates :attr:`Provider._all_provider_functions` by coordinating
with the :func:`provider` decorator.
Expand Down
4 changes: 2 additions & 2 deletions ci_exec/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ def build():
create : bool
Whether or not the ``dest`` is allowed to be created. Default: ``False``, the
``dest`` must exist already (will :func:`~ci_exec.core.fail` if it does not).
If ``True``, :func:`~ci_exec.core.mkdir_p` will be called with ``dest``.
``dest`` must exist already (will |fail| if it does not). If ``True``,
|mkdir_p| will be called with ``dest``.
"""

def __init__(self, dest: Union[str, Path], *, create: bool = False):
Expand Down

0 comments on commit c262902

Please sign in to comment.