From 893224b0555f2c8802c83d25ec58d9be523e9c5e Mon Sep 17 00:00:00 2001 From: Lee Netherton Date: Sat, 4 Apr 2020 22:41:49 +0100 Subject: [PATCH 1/2] Added documentation and readthedocs configuration --- .readthedocs.yml | 9 + .travis.yml | 1 + .vscode/launch.json | 24 ++ Makefile | 5 + README.md | 5 +- bin/prompty | 8 +- docs/_static/.gitignore | 0 docs/colours.rst | 5 + docs/conf.py | 179 +++++++++++++++ docs/functions.rst | 11 + docs/index.rst | 23 ++ docs/requirements.txt | 2 + environment.yml | 11 + prompty/colours.py | 144 ++++++++---- prompty/functions.py | 459 +++++++++++++++++++++++++++++++------- prompty/utils/__init__.py | 0 prompty/utils/sphinx.py | 84 +++++++ prompty/vcs.py | 34 +++ setup.py | 31 ++- test/test_colours.py | 2 +- 20 files changed, 894 insertions(+), 143 deletions(-) create mode 100644 .readthedocs.yml create mode 100644 .vscode/launch.json create mode 100644 docs/_static/.gitignore create mode 100644 docs/colours.rst create mode 100644 docs/conf.py create mode 100644 docs/functions.rst create mode 100644 docs/index.rst create mode 100644 docs/requirements.txt create mode 100644 environment.yml create mode 100644 prompty/utils/__init__.py create mode 100644 prompty/utils/sphinx.py diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 0000000..1927be4 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,9 @@ +version: 2 + +sphinx: + configuration: docs/conf.py + +python: + version: 3.7 + install: + - requirements: docs/requirements.txt diff --git a/.travis.yml b/.travis.yml index aad9eb5..402e6b7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,6 +15,7 @@ install: - pip install mock - pip install configparser - pip install pytest + - pip install sphinx script: - git fetch diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..cc2558a --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,24 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Sphinx Module", + "type": "python", + "request": "launch", + "module": "sphinx", + "args": [ + "-a", + "-E", + "docs", + "build/sphinx/html" + ] + }, + { + "name": "Python: Current File", + "type": "python", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal" + } + ] +} diff --git a/Makefile b/Makefile index a4801b6..e7ea411 100644 --- a/Makefile +++ b/Makefile @@ -35,6 +35,11 @@ upload: clean build install test: build $(PYTHON) $(SETUP) $@ +.PHONY: docs +docs: + $(PYTHON) $(SETUP) build_sphinx && \ + xdg-open build/sphinx/html/index.html + # Bespoke targets... .PHONY: clean clean: diff --git a/README.md b/README.md index 6d8cff4..e3e78f8 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -Prompty [![Build Status](https://travis-ci.org/ltn100/prompty.svg?branch=develop)](https://travis-ci.org/ltn100/prompty) -[![Coverage Status](https://coveralls.io/repos/ltn100/prompty/badge.svg?service=github)](https://coveralls.io/github/ltn100/prompty) [![PyPI version](https://badge.fury.io/py/prompty.svg)](https://pypi.org/project/prompty/) [![MIT license](http://img.shields.io/badge/license-MIT-blue.svg)](http://opensource.org/licenses/MIT) +Prompty [![Build Status](https://travis-ci.org/ltn100/prompty.svg?branch=develop)](https://travis-ci.org/ltn100/prompty) [![Documentation Status](https://readthedocs.org/projects/prompty/badge/?version=latest)](https://prompty.readthedocs.io/en/latest/) [![Coverage Status](https://coveralls.io/repos/ltn100/prompty/badge.svg?service=github)](https://coveralls.io/github/ltn100/prompty) [![PyPI version](https://badge.fury.io/py/prompty.svg)](https://pypi.org/project/prompty/) [![MIT license](http://img.shields.io/badge/license-MIT-blue.svg)](http://opensource.org/licenses/MIT) ======= Prompty is a [command prompt](https://en.wikipedia.org/wiki/Command-line_interface#Command_prompt) [markup language](https://en.wikipedia.org/wiki/Markup_language). @@ -124,4 +123,4 @@ For a more elaborate example, see [`~/.local/share/prompty/default.prompty`](./s # Documentation -Documentation will be coming soon... \ No newline at end of file +Documentation is available at [readthedocs](https://prompty.readthedocs.io/en/latest/). \ No newline at end of file diff --git a/bin/prompty b/bin/prompty index 56a757b..cdb9fe0 100755 --- a/bin/prompty +++ b/bin/prompty @@ -80,10 +80,10 @@ def main(argv=None): c = prompty.colours.Colours(prompty.functionContainer.FunctionContainer()) for style in c.STYLES: for colour in c.COLOURS: - print("%s%s : %s%s" % (c.startColour(colour, style=style, wrap=False), + print("%s%s : %s%s" % (c.startColour(colour, style=style, _wrap=False), style[c.NAME_KEY], colour[c.NAME_KEY], - c.stopColour(wrap=False))) + c.stopColour(_wrap=False))) return 0 if option in ("-d", "--debug"): @@ -97,10 +97,10 @@ def main(argv=None): fgcolour=colour[c.FG_KEY], bgcolour=colour[c.BG_KEY], style=colour[c.STYLE_KEY], - wrap=False + _wrap=False ), colour[c.NAME_KEY], - c.stopColour(wrap=False)) + c.stopColour(_wrap=False)) ) return 0 diff --git a/docs/_static/.gitignore b/docs/_static/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/docs/colours.rst b/docs/colours.rst new file mode 100644 index 0000000..ef99b39 --- /dev/null +++ b/docs/colours.rst @@ -0,0 +1,5 @@ +Colours +======= + +.. autoclass:: prompty.colours.Colours + :members: \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..3d998c6 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,179 @@ +# -*- coding: utf-8 -*- +# +# Configuration file for the Sphinx documentation builder. +# +# This file does only contain a selection of the most common options. For a +# full list see the documentation: +# http://www.sphinx-doc.org/en/master/config + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys +sys.path.insert(0, os.path.abspath('..')) + + +# -- Project information ----------------------------------------------------- + +project = u'prompty' +copyright = u'2020, Lee Netherton' +author = u'Lee Netherton' + +# The short X.Y version +version = u'' +# The full version, including alpha/beta/rc tags +release = u'0.2.1' + + +# -- General configuration --------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'prompty.utils.sphinx' +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = None + +autodoc_member_order = 'bysource' + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'sphinx_rtd_theme' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# The default sidebars (for documents that don't match any pattern) are +# defined by theme itself. Builtin themes are using these templates by +# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', +# 'searchbox.html']``. +# +# html_sidebars = {} + + +# -- Options for HTMLHelp output --------------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = 'promptydoc' + + +# -- Options for LaTeX output ------------------------------------------------ + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'prompty.tex', u'prompty Documentation', + u'Lee Netherton', 'manual'), +] + + +# -- Options for manual page output ------------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'prompty', u'prompty Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ---------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'prompty', u'prompty Documentation', + author, 'prompty', 'One line description of project.', + 'Miscellaneous'), +] + + +# -- Options for Epub output ------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = project + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +# +# epub_identifier = '' + +# A unique identification for the text. +# +# epub_uid = '' + +# A list of files that should not be packed into the epub file. +epub_exclude_files = ['search.html'] + + +# -- Extension configuration ------------------------------------------------- diff --git a/docs/functions.rst b/docs/functions.rst new file mode 100644 index 0000000..5a98925 --- /dev/null +++ b/docs/functions.rst @@ -0,0 +1,11 @@ +Functions +========= + +.. automodule:: prompty.functions + :members: + :undoc-members: + +.. automodule:: prompty.vcs + :members: + :undoc-members: + :exclude-members: VCS, VCSBase diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..c7efa79 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,23 @@ +Welcome to prompty's documentation! +=================================== + +.. toctree:: + :hidden: + + self + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + functions + colours + + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`search` diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..856b3f4 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,2 @@ +sphinx +sphinx_rtd_theme \ No newline at end of file diff --git a/environment.yml b/environment.yml new file mode 100644 index 0000000..46fcfa6 --- /dev/null +++ b/environment.yml @@ -0,0 +1,11 @@ +name: prompty-dev +dependencies: + - python==3.7.* + - future + - configparser + - mock + - pytest + - pycodestyle + - wheel + - sphinx<2 + - sphinx_rtd_theme<0.5 diff --git a/prompty/colours.py b/prompty/colours.py index 7af6a83..d913e34 100644 --- a/prompty/colours.py +++ b/prompty/colours.py @@ -40,7 +40,8 @@ class Colours(functionBase.PromptyFunctions): LCYAN = {NAME_KEY: "lightcyan", CODE_KEY: "lc", VAL_KEY: 96} WHITE = {NAME_KEY: "white", CODE_KEY: "w", VAL_KEY: 97} COLOURS = [BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, LGREY, - DGREY, LRED, LGREEN, LYELLOW, LBLUE, LMAGENTA, LCYAN, WHITE] + DGREY, LRED, LGREEN, LYELLOW, LBLUE, LMAGENTA, LCYAN, WHITE, + DEFAULT] # 4-bit colour background offset BG_OFFSET = 10 @@ -97,7 +98,7 @@ def _encode(self, code, wrap=True): INFO1 = {NAME_KEY: 'info1', FG_KEY: GREEN, BG_KEY: None, STYLE_KEY: None} INFO2 = {NAME_KEY: 'info2', FG_KEY: LBLUE, BG_KEY: None, STYLE_KEY: None} INFO3 = {NAME_KEY: 'info3', FG_KEY: LMAGENTA, BG_KEY: None, STYLE_KEY: None} - PALETTE = [DEFAULT, DIM1, DIM2, DIM3, BRIGHT, ERROR, WARNING, INFO1, INFO2, INFO3] + PALETTE = [DIM1, DIM2, DIM3, BRIGHT, ERROR, WARNING, INFO1, INFO2, INFO3] def _setPalette(self, identifier, fgcolour=None, bgcolour=None, style=None): if identifier is None: @@ -331,33 +332,8 @@ def _getStyleCode(self, identifier): raise KeyError("No such style %s" % str(identifier)) - def startColour(self, fgcolour=None, bgcolour=None, style=None, wrap=True): - colourCode = "" - - if style: - colourCode += str(self._getStyleCode(style)) - - if fgcolour: - if colourCode: - colourCode += ";" - colourCode += str(self._getColourCode(fgcolour, self.FOREGROUND)) - - if bgcolour: - if colourCode: - colourCode += ";" - colourCode += str(self._getColourCode(bgcolour, self.BACKGROUND)) - - return self._encode(colourCode, wrap=wrap) - - def stopColour(self, wrap=True): - return self._encode(self.RESET_KEY, wrap=wrap) - - def colour(self, literal, fgcolour=None, bgcolour=None, style=None, wrap=True): - return self.startColour(fgcolour=fgcolour, bgcolour=bgcolour, style=style, wrap=wrap) + \ - literal + \ - self.stopColour(wrap=wrap) - - def _colourFuncFactory(self, clr): + @staticmethod + def _colourFuncFactory(clr): def fgfunc(slf, literal, style=None): return slf.startColour(fgcolour=clr, style=style) + \ literal + \ @@ -367,23 +343,49 @@ def bgfunc(slf, literal, style=None): return slf.startColour(bgcolour=clr, style=style) + \ literal + \ slf.stopColour() + + fgfunc.__doc__ = """ + Set {} foreground colour for ``literal``. + + :param style: The character style. + """.format(clr) + + bgfunc.__doc__ = """ + Set {} background colour for ``literal``. + + :param style: The character style. + """.format(clr) + return fgfunc, bgfunc - def _styleFuncFactory(self, style): + @staticmethod + def _styleFuncFactory(style): def func(slf, literal): return slf.startColour(style=style) + \ literal + \ slf.stopColour() + + func.__doc__ = """ + Set the style to {} for ``literal``. + """.format(style) + return func - def _paletteFuncFactory(self, pal): + @staticmethod + def _paletteFuncFactory(pal): def func(slf, literal): return slf.startColour(fgcolour=pal) + \ literal + \ slf.stopColour() + + func.__doc__ = """ + Set the pallet colour to {} for ``literal``. + """.format(pal) + return func - def _populateFunctions(self): + @staticmethod + def _populateFunctions(): """ This will define functions for all 4-bit colours. The function definitions are of the form: @@ -393,20 +395,70 @@ def _populateFunctions(self): bold(literal) """ - for c in self.COLOURS: - colourName = c[self.NAME_KEY] - fgfunc, bgfunc = self._colourFuncFactory(colourName) - setattr(self, colourName, types.MethodType(fgfunc, self)) - setattr(self, colourName+"bg", types.MethodType(bgfunc, self)) - for s in self.STYLES: - styleName = s[self.NAME_KEY] - func = self._styleFuncFactory(styleName) - setattr(self, styleName, types.MethodType(func, self)) - for p in self.PALETTE: - paletteName = p[self.NAME_KEY] - func = self._paletteFuncFactory(paletteName) - setattr(self, paletteName, types.MethodType(func, self)) + for c in Colours.COLOURS: + colourName = c[Colours.NAME_KEY] + fgfunc, bgfunc = Colours._colourFuncFactory(colourName) + setattr(Colours, colourName, fgfunc) + setattr(Colours, colourName+"bg", bgfunc) + for s in Colours.STYLES: + styleName = s[Colours.NAME_KEY] + func = Colours._styleFuncFactory(styleName) + setattr(Colours, styleName, func) + for p in Colours.PALETTE: + paletteName = p[Colours.NAME_KEY] + func = Colours._paletteFuncFactory(paletteName) + setattr(Colours, paletteName, func) + + # ------------------------ + # Public methods + # ------------------------ + def startColour(self, fgcolour=None, bgcolour=None, style=None, _wrap=True): + """ + Start a colour block. + + :param fgcolour: The foreground colour. + :param bgcolour: The background colour. + :param style: The character style. + """ + colourCode = "" + + if style: + colourCode += str(self._getStyleCode(style)) + + if fgcolour: + if colourCode: + colourCode += ";" + colourCode += str(self._getColourCode(fgcolour, self.FOREGROUND)) + + if bgcolour: + if colourCode: + colourCode += ";" + colourCode += str(self._getColourCode(bgcolour, self.BACKGROUND)) + + return self._encode(colourCode, wrap=_wrap) + + def stopColour(self, _wrap=True): + """ + Stop a colour block. + + :param wrap: Wrap the code in additional characters + to signify non-printing characters are + contained + """ + return self._encode(self.RESET_KEY, wrap=_wrap) + + def colour(self, literal, fgcolour=None, bgcolour=None, style=None, _wrap=True): + """ + Wrap the string ``literal`` in a colour block. The colour is stopped when ``literal`` ends. + + :param fgcolour: The foreground colour. + :param bgcolour: The background colour. + :param style: The character style. + """ + return self.startColour(fgcolour=fgcolour, bgcolour=bgcolour, style=style, _wrap=_wrap) + \ + literal + \ + self.stopColour(_wrap=_wrap) # Populate the functions in this module -# _populateFunctions(sys.modules[__name__]) +Colours._populateFunctions() diff --git a/prompty/functions.py b/prompty/functions.py index 2e89fd1..9445d49 100644 --- a/prompty/functions.py +++ b/prompty/functions.py @@ -18,100 +18,284 @@ from . import functionBase -# TO DO: -# \a an ASCII bell character (07) -# \j the number of jobs currently managed by the shell -# \l the basename of the shell's terminal device name -# \s the name of the shell, the basename of $0 (the portion -# following the final slash) -# \t the current time in 24-hour HH:MM:SS format -# \T the current time in 12-hour HH:MM:SS format -# \@ the current time in 12-hour am/pm format -# \A the current time in 24-hour HH:MM format -# \v the version of bash (e.g., 2.00) -# \V the release of bash, version + patch level (e.g., 2.00.0) -# \! the history number of this command -# \# the command number of this command - - -class BaseFunctions(functionBase.PromptyFunctions): - # ----- Special Characters -------- - # \e +class SpecialCharacters(functionBase.PromptyFunctions): + """ + Functions to print special characters. + """ def unichar(self, code): + """ + Generate a unicode character. + + :param code: A unicode integer value. Can be prepended with ``0x`` or + ``0o`` for hexadecimal or octal values. + """ return chr(int(code, 0)) - # \\ def backslash(self): + """ + A backslash character (``\\``). + """ return "\\" def percent(self): + """ + A percent character (``%``). + """ return "%" def opencurly(self): + """ + An open curly brace character (``{``). + """ return "{" def closecurly(self): + """ + An close curly brace character (``}``). + """ return "}" def opensquare(self): + """ + An open square bracket character (``[``). + """ return "[" def closesquare(self): + """ + A close square bracket character (``]``). + """ return "]" def space(self): + """ + A single "space" character. + """ return " " - # \n def newline(self): + """ + A new line character (``\\n``). + """ return "\n" - # \r def carriagereturn(self): + """ + A carriage return character (``\\r``). + """ return "\r" - # \e def escape(self): + """ + A bash escape code character (``\\033``). + """ return str("\033") - # ----- Bash Prompt Functions -------- - # \D - def datefmt(self, fmt=None): - now = datetime.datetime.now() - if fmt: - fmt = fmt.replace('#', '%') - return now.strftime(fmt) - else: - return now.strftime("%X") + def tick(self): + """ + A tick symbol (\u2714). + """ + return chr(0x2714) + + def cross(self): + """ + A cross symbol (\u2718). + """ + return chr(0x2718) + + def highvoltage(self): + """ + A high voltage symbol (\u26A1). + """ + return chr(0x26A1) + + +class PowerlineFunctions(functionBase.PromptyFunctions): + """ + Functions for use with + `Powerline `_ fonts. + + You must have the `powerline fonts `_ + package installed in order for these to render properly. + """ + def powerline(self, content, bg="blue", bg_next="default", fg="white", dir="right"): + """ + Render ``content`` inside powerline arrows. It is possible to string + together multiple powerlines together by ensuring that the next + background colour is set match the next block's background. For + example: + + .. highlight:: python + .. code-block:: latex + + \\powerline[27][33]{this} % colour 33 is carried over + \\powerline[33][75]{is} % colour 75 is carried over + \\powerline[75]{cool} % next colour is default bg colour + + :param content: The contents of the powerline + :param bg: Background colour, defaults to "blue" + :param bg_next: Background colour of next block, defaults to "default" + :param fg: Foreground colour, defaults to "white" + :param dir: The arrow direction, defaults to "right" + """ + out = "" + if dir == "left": + out += self.call("startColour", bgcolour=bg_next) + out += self.call("startColour", fgcolour=bg) + out += self.plleftarrowfill() + out += self.call("startColour", fgcolour=fg) + out += self.call("startColour", bgcolour=bg) + out += " " + out += content + out += " " + if dir == "right": + out += self.call("startColour", bgcolour=bg_next) + out += self.call("startColour", fgcolour=bg) + out += self.plrightarrowfill() + out += self.call("stopColour") + return out + + def plbranch(self): + """ + A powerline branch symbol. + """ + return chr(0xe0a0) + + def plline(self): + """ + A powerline "line number" symbol. + """ + return chr(0xe0a1) + + def pllock(self): + """ + A powerline padlock symbol. + """ + return chr(0xe0a2) + + def plrightarrowfill(self): + """ + A powerline filled right arrow. + """ + return chr(0xe0b0) + + def plrightarrow(self): + """ + A powerline unfilled right arrow. + """ + return chr(0xe0b1) + + def plleftarrowfill(self): + """ + A powerline filled left arrow. + """ + return chr(0xe0b2) + + def plleftarrow(self): + """ + A powerline unfilled left arrow. + """ + return chr(0xe0b3) + + +class BashPromptEscapes(functionBase.PromptyFunctions): + """ + Functions to mimic bash prompt escape sequences, similar to those defined + here: + https://www.tldp.org/HOWTO/Bash-Prompt-HOWTO/bash-prompt-escape-sequences.html + """ + # TODO: + # - \a - an ASCII bell character (07) + # - \j - the number of jobs currently managed by the shell + # - \l - the basename of the shell's terminal device name + # - \s - the name of the shell, the basename of $0 (the portion + # following the final slash) + # - \t - the current time in 24-hour HH:MM:SS format + # - \T - the current time in 12-hour HH:MM:SS format + # - \@ - the current time in 12-hour am/pm format + # - \A - the current time in 24-hour HH:MM format + # - \v - the version of bash (e.g., 2.00) + # - \V - the release of bash, version + patch level (e.g., 2.00.0) + # - \! - the history number of this command + # - \# - the command number of this command - # \d def date(self): + """ + The date in "Weekday Month Date" format (e.g., ``Tue May 26``). + + Equivalent to the bash prompt escape sequence ``\\d`` + """ return self.datefmt("%a %b %d") - # \u + def datefmt(self, fmt='#X'): + """ + Generate the current date and time, and format it according to the + given ``fmt`` string. Formats are given in the `strptime() + `_ + function documentation. However, because ``%`` is a special character + in prompty, the control character is changed to ``#``. For example: + ``#a #b #d`` will render to "Weekday Month Date". + + If no ``fmt`` string is given, then the local time in 24hr format is + returned. + + :param fmt: format string, defaults to ``#X`` - Locale's appropriate time + representation, e.g.: ``21:30:00``. + """ + now = datetime.datetime.now() + fmt = fmt.replace('#', '%') + return now.strftime(fmt) + def user(self): + """ + The username of the current user. + + Equivalent to the bash prompt escape sequence ``\\u``. + """ return getpass.getuser() - # \h def hostname(self): + """ + The hostname up to the first ``.``. + + Equivalent to the bash prompt escape sequence ``\\h``. + """ return socket.gethostname().split(".")[0] - # \H def hostnamefull(self): + """ + The hostname. + + Equivalent to the bash prompt escape sequence ``\\H``. + """ return socket.gethostname() - # \w def workingdir(self): + """ + The current working directory. + + Equivalent to the bash prompt escape sequence ``\\w``. + """ cwd = self.status.getWorkingDir() home = os.path.expanduser(r"~") return re.sub(r'^%s' % home, r"~", cwd) - # \W def workingdirbase(self): + """ + The basename of the current working directory. + + Equivalent to the bash prompt escape sequence ``\\W``. + """ return os.path.basename(self.status.getWorkingDir()) - # \$ def dollar(self, euid=None): + """ + If the effective UID is 0, a ``#``, otherwise a ``$``. + + Equivalent to the bash prompt escape sequence ``\\$``. + + :param euid: A user Id, defaults to the current user. + """ if euid is None: euid = self.status.euid if int(euid) == 0: @@ -119,7 +303,16 @@ def dollar(self, euid=None): else: return str(r"$") + +class MiscFunctions(functionBase.PromptyFunctions): def isrealpath(self, path=None): + """ + If the current directory is a real path (i.e. not via a symbolic link) + then return ``True``, else return ``False``. + + :param path: A path, defaults to the current working path. + :rtype: bool + """ if path is None: path = self.status.getWorkingDir() if path == os.path.realpath(path): @@ -129,29 +322,103 @@ def isrealpath(self, path=None): # ----- Expression Functions -------- def exitsuccess(self): + """ + If the last command executed with a 0 status code, return ``True``. Otherwise + return ``False``. + + :rtype: bool + """ if self.status.exitCode == 0: return True else: return False def equals(self, a, b): + """ + Return ``True`` if the two parameters ``a`` and ``b`` are equal (by value). + + :rtype: bool + """ return a == b def max(self, a, b): + """ + Return the maximum of ``a`` and ``b``. + + :type a: number + :type b: number + :rtype: number + """ if float(a) > float(b): return a else: return b + def min(self, a, b): + """ + Return the minimum of ``a`` and ``b``. + + :type a: number + :type b: number + :rtype: number + """ + if float(a) < float(b): + return a + else: + return b + def gt(self, a, b): + """ + Return ``True`` if ``a`` > ``b``. + + :type a: number + :type b: number + :rtype: bool + """ return float(a) > float(b) def lt(self, a, b): + """ + Return ``True`` if ``a`` < ``b``. + + :type a: number + :type b: number + :rtype: bool + """ return float(a) < float(b) + def gte(self, a, b): + """ + Return ``True`` if ``a`` >= ``b``. + + :type a: number + :type b: number + :rtype: bool + """ + return float(a) >= float(b) + + def lte(self, a, b): + """ + Return ``True`` if ``a`` <= ``b``. + + :type a: number + :type b: number + :rtype: bool + """ + return float(a) <= float(b) + # ----- Control Functions -------- - def ifexpr(self, cond, thenval, elseval=None): + def ifexpr(self, cond, thenval='', elseval=''): + """ + If ``cond`` is equivalent to ``True``, then return ``thenval``, else + return ``elseval``. + + :param cond: Condition + :type cond: bool + :param thenval: Value returned if condition is equivalent to ``True``, defaults to an empty string. + :param elseval: Value returned if condition is equivalent to ``False``, defaults to an empty string. + """ if _tobool(cond): return thenval else: @@ -162,9 +429,36 @@ def ifexpr(self, cond, thenval, elseval=None): # ----- String Functions -------- def lower(self, literal): + """ + Return a lowercase representation of ``literal``. + """ return str(literal).lower() + def upper(self, literal): + """ + Return an uppercase representation of ``literal``. + """ + return str(literal).upper() + def join(self, *args): + """ + Join multiple strings together. The first argument is the delimiter, all subsequent + arguments are strings to join. + + Example: + + .. highlight:: python + .. code-block:: latex + + \\join{_}{joined}{with}{underscores} + + Output: + + .. highlight:: python + .. code-block:: none + + joined_with_underscores + """ if len(args) < 1: raise TypeError("join needs at least one argument") delim = args[0] @@ -172,6 +466,27 @@ def join(self, *args): return str(delim).join(args) def justify(self, left, centre, right, lpad=u" ", rpad=u" "): + """ + Justify text in 3 columns to fill the terminal. Text in ``left`` will be + left justified, text in ``centre`` will be centre justified, and text in + ``right`` will be right justified. The padding characters between ``left`` + and ``centre``, and ``centre`` and ``right`` can be controlled with ``lpad`` + and ``rpad``, respectively. + + Example: + + .. highlight:: python + .. code-block:: latex + + \\justify[_]{this is left}{in the centre}{on the right} + + Output: + + .. highlight:: python + .. code-block:: none + + this is left________________________in the centre on the right + """ sleft = len(left) scentre = len(centre) sright = len(right) @@ -189,52 +504,20 @@ def justify(self, left, centre, right, lpad=u" ", rpad=u" "): return left + lpad*lpadsize + centre + rpad*rpadsize + right def right(self, literal): + """ + Justify string ``literal`` right. + """ return self.justify("", "", literal) - # ----- Misc Functions -------- - def tick(self): - return chr(0x2714) - - def cross(self): - return chr(0x2718) - - def highvoltage(self): - return chr(0x26A1) - - def plbranch(self): - return chr(0xe0a0) - - def plline(self): - return chr(0xe0a1) - - def pllock(self): - return chr(0xe0a2) - - def plrightarrowfill(self): - return chr(0xe0b0) - - def plrightarrow(self): - return chr(0xe0b1) - - def plleftarrowfill(self): - return chr(0xe0b2) - - def plleftarrow(self): - return chr(0xe0b3) - - def powerline(self, content, background="blue", foreground="white"): - out = self.call("startColour", fgcolour=foreground) - out += self.call("startColour", bgcolour=background) - out += " " - out += content - out += " " - out += self.call("startColour", bgcolour=foreground) - out += self.call("startColour", fgcolour=background) - out += self.plrightarrowfill() - out += self.call("stopColour") - return out - def smiley(self): + """ + Generate a smiley that has the following properties: + + - ``$:)`` - (colour green) Last command succeeded, user is not root + - ``$:(`` - (colour red) Last command failed, user is not root + - ``#:)`` - (colour green) Last command succeeded, user is root + - ``#:(`` - (colour red) Last command failed, user is root + """ if self.status.exitCode == 0: out = self.call("startColour", "green", style="bold") out += self.call("dollar") @@ -247,6 +530,11 @@ def smiley(self): return out def randomcolour(self, literal, seed=None): + """ + Decorate ``literal`` with a random colour. + + :param seed: The random hash seed, to enable consistency between calls. + """ if seed: random.seed(seed) colour = str(random.randrange(1, 255)) @@ -256,6 +544,11 @@ def randomcolour(self, literal, seed=None): return out def hashedcolour(self, literal): + """ + Decorate ``literal`` with a colour based on its hash. + This can be useful for generating a unique colour for different + user or host names. + """ return self.randomcolour(literal, seed=literal) diff --git a/prompty/utils/__init__.py b/prompty/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/prompty/utils/sphinx.py b/prompty/utils/sphinx.py new file mode 100644 index 0000000..bc15553 --- /dev/null +++ b/prompty/utils/sphinx.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python +# vim:set softtabstop=4 shiftwidth=4 tabstop=4 expandtab: +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import str + +import sphinx.util.inspect +from sphinx.ext.autodoc import MethodDocumenter, ModuleDocumenter, ClassDocumenter, Signature +from six import StringIO, itervalues + +from .. import functionBase + + +class PromptySignature(sphinx.util.inspect.Signature): + def format_args(self): + """ + Format the arguments like ``[oparg][optarg=default]{reqarg}``. + """ + required_args = [] + optional_args = [] + for i, param in enumerate(itervalues(self.parameters)): + # skip first argument if subject is bound method + if self.skip_first_argument and i == 0: + continue + + arg = StringIO() + + if param.default is not param.empty: + # Optional arg [] + if param.name.startswith("_"): + # Skip documenting optional parameters that start with _ + continue + + arg.write("[") + arg.write(param.name) + if param.default is not None: + arg.write("='") + arg.write(str(param.default)) + arg.write("'") + arg.write("]") + optional_args.append(arg.getvalue()) + else: + # Required arg {} + arg.write("{") + arg.write(param.name) + arg.write("}") + required_args.append(arg.getvalue()) + + return ''.join(optional_args) + ''.join(required_args) + + +class PromptyMethodDocumenter(MethodDocumenter): + objtype = 'promptymethod' + priority = 20 # higher priority than MethodDocumenter + + def format_args(self, **kwargs): + """ + Format the arguments like ``[oparg][optarg=default]{reqarg}``. + """ + # Monkey patch the Signature class + OldSignature = sphinx.ext.autodoc.Signature + sphinx.ext.autodoc.Signature = PromptySignature + + # Call base class member + args = super(PromptyMethodDocumenter, self).format_args(**kwargs) + + # Revert patch + sphinx.ext.autodoc.Signature = OldSignature + return args + + def format_name(self, **kwargs): + """ + Format the function name like ``\\function``. + """ + name = super(PromptyMethodDocumenter, self).format_name(**kwargs) + # Remove class name + name = name.split('.')[-1] + return "\\\\" + name + + +def setup(app): + app.add_autodocumenter(PromptyMethodDocumenter) diff --git a/prompty/vcs.py b/prompty/vcs.py index 20f3aa4..1c3eef6 100644 --- a/prompty/vcs.py +++ b/prompty/vcs.py @@ -122,37 +122,71 @@ def runCommand(self, cmdList): class VCSFunctions(functionBase.PromptyFunctions): def isrepo(self): + """ + Return ``True`` if the current working directory is a version control repository. + + :rtype: bool + """ return self.status.vcs.isRepo def repobranch(self): + """ + The repository branch name. + """ return self.status.vcs.branch def isrepodirty(self): + """ + Return ``True`` if the repository has uncommited modifications. + + :rtype: bool + """ if self.status.vcs.changed + self.status.vcs.staged + self.status.vcs.unmerged > 0: return True else: return False def ahead(self): + """ + Get the number of commits ahead of the remote repository. + """ return self.status.vcs.ahead def behind(self): + """ + Get the number of commits behind the remote repository. + """ return self.status.vcs.behind def commit(self): return self.status.vcs.commit def staged(self): + """ + Get the number of files that are currently staged. + """ return self.status.vcs.staged def changed(self): + """ + Get the number of files that are modified and not staged. + """ return self.status.vcs.changed def untracked(self): + """ + Get the number of untracked files that are in the repository (excluding those ignored). + """ return self.status.vcs.untracked def last_fetched(self): + """ + Get the time, in seconds, since the remote was last fetched. + """ return self.status.vcs.last_fetched def last_fetched_min(self): + """ + Get the time, in minutes, since the remote was last fetched. + """ return self.status.vcs.last_fetched // 60 diff --git a/setup.py b/setup.py index 8fd2fe4..d478141 100644 --- a/setup.py +++ b/setup.py @@ -3,6 +3,7 @@ import setuptools from setuptools.command.build_py import build_py +from sphinx.setup_command import BuildDoc import py_compile import glob import os @@ -33,9 +34,20 @@ def run(self): py_compile.compile('bin/prompty') +cmdclass = { + # Post build step + "build_py": PostBuild, + + # Build docs + 'build_sphinx': BuildDoc, +} + +name = "prompty" +version = find_version("prompty", "__init__.py") + setuptools.setup( - name="prompty", - version=find_version("prompty", "__init__.py"), + name=name, + version=version, description="A command line prompt markup language", author="Lee Netherton", author_email="lee.netherton@gmail.com", @@ -75,13 +87,20 @@ def run(self): ), ], - cmdclass={ - # Post build step - "build_py": PostBuild, + cmdclass=cmdclass, + + command_options={ + 'build_sphinx': { + 'project': ('setup.py', name), + 'version': ('setup.py', version), + 'source_dir': ('setup.py', 'docs') + } }, setup_requires=[ - "wheel" + "wheel", + "sphinx", + "sphinx_rtd_theme" ], install_requires=[ diff --git a/test/test_colours.py b/test/test_colours.py index 7678f63..385583b 100644 --- a/test/test_colours.py +++ b/test/test_colours.py @@ -90,7 +90,7 @@ def test_stopColour(self): def test_startColour(self): c = prompty.colours.Colours(None) self.assertEqual(c.startColour("green"), "\001\033[32m\002") - self.assertEqual(c.startColour("green", wrap=False), "\033[32m") + self.assertEqual(c.startColour("green", _wrap=False), "\033[32m") self.assertEqual(c.startColour("red", style="b"), "\001\033[1;31m\002") self.assertEqual(c.startColour("1"), "\001\033[38;5;1m\002") self.assertEqual(c.startColour(fgcolour="1", bgcolour="2"), "\001\033[38;5;1;48;5;2m\002") From 32ce9d8aa677bb2a838a357fa5a7d32314da1bda Mon Sep 17 00:00:00 2001 From: Lee Netherton Date: Thu, 9 Apr 2020 21:21:55 +0100 Subject: [PATCH 2/2] Bump version --- docs/conf.py | 2 +- prompty/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 3d998c6..11294e6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -26,7 +26,7 @@ # The short X.Y version version = u'' # The full version, including alpha/beta/rc tags -release = u'0.2.1' +release = u'0.3.0' # -- General configuration --------------------------------------------------- diff --git a/prompty/__init__.py b/prompty/__init__.py index 7f95534..74c3875 100644 --- a/prompty/__init__.py +++ b/prompty/__init__.py @@ -2,7 +2,7 @@ # vim:set softtabstop=4 shiftwidth=4 tabstop=4 expandtab: # Must comply with http://legacy.python.org/dev/peps/pep-0440/#version-scheme -__version__ = "0.2.1" +__version__ = "0.3.0" from . import prompt from . import functions