Skip to content

Commit

Permalink
Merge pull request #44 from AuHau/master
Browse files Browse the repository at this point in the history
Introduction of Path question
  • Loading branch information
magmax authored Aug 21, 2018
2 parents 85af721 + 0c6d4b8 commit f753ee4
Show file tree
Hide file tree
Showing 11 changed files with 395 additions and 17 deletions.
11 changes: 11 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[*]
charset = utf-8
insert_final_newline = true

[*.py]
indent_style = space
indent_size = 4
max_line_length = 79

[Makefile]
indent_style = tab
19 changes: 19 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,25 @@ Example:
|inquirer checkbox|

Path
--------

Like Text question, but with builtin validations for working with paths.

Example:

.. code:: python
import inquirer
questions = [
inquirer.Path('log_file',
message="Where logs should be located?",
path_type=inquirer.Path.DIRECTORY,
),
]
answers = inquirer.prompt(questions)
License
=======

Expand Down
16 changes: 16 additions & 0 deletions docs/source/inquirer.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,22 @@ inquirer.questions module
:undoc-members:
:show-inheritance:

inquirer.shortcuts module
-------------------------

.. automodule:: inquirer.shortcuts
:members:
:undoc-members:
:show-inheritance:

inquirer.themes module
----------------------

.. automodule:: inquirer.themes
:members:
:undoc-members:
:show-inheritance:


Module contents
---------------
Expand Down
74 changes: 63 additions & 11 deletions docs/source/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,19 @@ Each :code:`Question` require some common arguments. So, you just need to know w
Question types
--------------

+-------------+--------------------------------------------------+
|**TEXT** | Expects a text answer |
+-------------+--------------------------------------------------+
|**PASSWORD** | Do not prompt the answer. |
+-------------+--------------------------------------------------+
|**CONFIRM** | Requires a boolean answer |
+-------------+--------------------------------------------------+
|**LIST** | Show a list and allow to select just one answer. |
+-------------+--------------------------------------------------+
|**CHECKBOX** | Show a list and allow to select a bunch of them |
+-------------+--------------------------------------------------+
+-------------+--------------------------------------------------------+
|**TEXT** | Expects a text answer. |
+-------------+--------------------------------------------------------+
|**PASSWORD** | Do not prompt the answer. |
+-------------+--------------------------------------------------------+
|**CONFIRM** | Requires a boolean answer. |
+-------------+--------------------------------------------------------+
|**LIST** | Show a list and allow to select just one answer. |
+-------------+--------------------------------------------------------+
|**CHECKBOX** | Show a list and allow to select a bunch of them. |
+-------------+--------------------------------------------------------+
|**PATH** | Requires valid path and allows additional validations. |
+-------------+--------------------------------------------------------+

There are pictures of some of them in the :ref:`examples` section.

Expand Down Expand Up @@ -139,6 +141,56 @@ It's value is `boolean` or a `function` with the sign:
where ``answers`` contains the `dict` of previous answers again.


Path Question
-------------

Path Question accepts any valid path which can be both absolute or relative.
By default it only validates the validity of the path. Except of validation
it return normalized path and it expands home alias (~).

The Path Question have additional arguments for validating paths.

path_type
~~~~~~~~~

Validation argument that enables to enforce if the path should be aiming
to file (``Path.FILE``) or directory (``Path.DIRECTORY``).

By default nothing is enforced (``Path.ANY``).

.. code:: python
Path('log_file', 'Where should be log files located?', path_type=Path.DIRECTORY)
exists
~~~~~~

Validation argument that enables to enforce if the provided path should
or should not exists. Expects ``True`` if the path should
exists, or ``False`` if the path should not exists.

By default nothing is enforced (``None``)

.. code:: python
Path('config_file', 'Point me to your configuration file.', exists=True, path_type=Path.File)
normalize_to_absolute_path
~~~~~~~~~~~~~~~~~~~~~~~~~~

Argument which will enable normalization on the provided path. When enabled, in case of relative path would be provided
the Question will normalize it to absolute path.

Expects ``bool`` value. Default ``False``.

.. code:: python
Path('config_file', 'Point me to your configuration file.', normalize_to_absolute_path=True)
Creating the Question object
----------------------------

Expand Down
6 changes: 3 additions & 3 deletions inquirer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@

try:
from .prompt import prompt
from .questions import Text, Password, Confirm, List, Checkbox, \
load_from_dict, load_from_json
from .questions import Text, Password, Confirm, List, Checkbox, Path, \
load_from_dict, load_from_json, load_from_list
from .shortcuts import text, password, confirm, list_input, checkbox

__all__ = ['prompt', 'Text', 'Password', 'Confirm', 'List', 'Checkbox',
'load_from_list', 'load_from_dict', 'load_from_json',
'Path', 'load_from_list', 'load_from_dict', 'load_from_json',
'text', 'password', 'confirm', 'list_input', 'checkbox']
except ImportError as e:
print("An error was found, but returning just with the version: %s" % e)
108 changes: 107 additions & 1 deletion inquirer/questions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@
"""

import json
import os
import errno
import sys

from . import errors


def question_factory(kind, *args, **kwargs):
for clazz in (Text, Password, Confirm, List, Checkbox):
for clazz in (Text, Password, Confirm, List, Checkbox, Path):
if clazz.kind == kind:
return clazz(*args, **kwargs)
raise errors.UnknownQuestionTypeError()
Expand Down Expand Up @@ -169,3 +172,106 @@ def __init__(self,

class Checkbox(Question):
kind = 'checkbox'


# Solution for checking valid path based on
# https://stackoverflow.com/a/34102855/1360532
ERROR_INVALID_NAME = 123


def is_pathname_valid(pathname):
"""
`True` if the passed pathname is a valid pathname for the current OS;
`False` otherwise.
"""
try:
if not isinstance(pathname, str) or not pathname:
return False

_, pathname = os.path.splitdrive(pathname)

root_dirname = os.environ.get('HOMEDRIVE', 'C:') \
if sys.platform == 'win32' else os.path.sep

assert os.path.isdir(root_dirname)
root_dirname = root_dirname.rstrip(os.path.sep) + os.path.sep

for pathname_part in pathname.split(os.path.sep):
try:
os.lstat(root_dirname + pathname_part)
except OSError as exc:
if hasattr(exc, 'winerror'):
if exc.winerror == ERROR_INVALID_NAME:
return False
elif exc.errno in {errno.ENAMETOOLONG, errno.ERANGE}:
return False
except (ValueError, TypeError):
return False
else:
return True


class Path(Text):
ANY = 'any'
FILE = 'file'
DIRECTORY = 'directory'

kind = 'path'

def __init__(self, name, default=None, path_type='any', exists=None,
normalize_to_absolute_path=False, **kwargs):
super(Path, self).__init__(name, default=default, **kwargs)

self._path_type = path_type
self._exists = exists
self._normalize_to_absolute_path = normalize_to_absolute_path

if default is not None:
try:
self.validate(default)
except errors.ValidationError:
raise ValueError("Default value '{}' is not valid based on "
"your Path's criteria".format(default))

def validate(self, current):
super(Path, self).validate(current)

if current is None:
raise errors.ValidationError(current)

if not is_pathname_valid(current):
raise errors.ValidationError(current)

# os.path.isdir and isfile check also existence of the path,
# which might not be desirable
if self._path_type == 'file':
if self._exists is None and os.path.basename(current) == '':
raise errors.ValidationError(current)
elif self._exists and not os.path.isfile(current):
raise errors.ValidationError(current)
elif self._exists is not None and not self._exists \
and os.path.isfile(current):
raise errors.ValidationError(current)

elif self._path_type == 'directory':
if self._exists is None and os.path.basename(current) != '':
raise errors.ValidationError(current)
elif self._exists and not os.path.isdir(current):
raise errors.ValidationError(current)
elif self._exists is not None and not self._exists \
and os.path.isdir(current):
raise errors.ValidationError(current)

elif self._exists and not os.path.exists(current):
raise errors.ValidationError(current)
elif self._exists is not None and not self._exists \
and os.path.exists(current):
raise errors.ValidationError(current)

def normalize_value(self, value):
value = os.path.expanduser(value)

if self._normalize_to_absolute_path:
value = os.path.abspath(value)

return value
2 changes: 2 additions & 0 deletions inquirer/render/console/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from ._confirm import Confirm
from ._list import List
from ._checkbox import Checkbox
from ._path import Path


class ConsoleRender(object):
Expand Down Expand Up @@ -152,6 +153,7 @@ def render_factory(self, question_type):
'confirm': Confirm,
'list': List,
'checkbox': Checkbox,
'path': Path,
}

if question_type not in matrix:
Expand Down
35 changes: 35 additions & 0 deletions inquirer/render/console/_path.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-

from readchar import key
from .base import BaseConsoleRender
from inquirer import errors


class Path(BaseConsoleRender):
title_inline = True

def __init__(self, *args, **kwargs):
super(Path, self).__init__(*args, **kwargs)
self.current = self.question.default or ''

def get_current_value(self):
return self.current

def process_input(self, pressed):
if pressed == key.CTRL_C:
raise KeyboardInterrupt()

if pressed in (key.CR, key.LF, key.ENTER):
raise errors.EndOfInput(
self.question.normalize_value(self.current)
)

if pressed == key.BACKSPACE:
if len(self.current):
self.current = self.current[:-1]
return

if len(pressed) != 1:
return

self.current += pressed
6 changes: 6 additions & 0 deletions inquirer/shortcuts.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,9 @@ def checkbox(message, render=None, **kwargs):
render = render or ConsoleRender()
question = questions.Checkbox(name='', message=message, **kwargs)
return render.render(question)


def path(message, render=None, **kwargs):
render = render or ConsoleRender()
question = questions.Path(name='', message=message, **kwargs)
return render.render(question)
4 changes: 2 additions & 2 deletions tests/unit/test_prompt.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import unittest
try:
from unittest import MagicMock, Mock
from unittest.mock import MagicMock, Mock
except ImportError:
from mock import MagicMock, Mock

Expand All @@ -21,5 +21,5 @@ def test_prompt_renders_a_questions(self):
result = prompt([question1], render=render)

self.assertEquals({'foo': result1}, result)
render.render.assert_called()
self.assertTrue(render.render.called)
render.render.call_args_list[0][0] == result1
Loading

0 comments on commit f753ee4

Please sign in to comment.