Skip to content

Commit

Permalink
path: simplify validation
Browse files Browse the repository at this point in the history
  • Loading branch information
Cube707 committed Jun 21, 2024
1 parent d03e29e commit ed5aa4b
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 140 deletions.
11 changes: 0 additions & 11 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,17 +227,6 @@ By default nothing is enforced (`None`)
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`.

```python
Path('config_file', 'Point me to your configuration file.', normalize_to_absolute_path=True)
```

## Creating the Question object

With this information, it is easy to create a `Question` object:
Expand Down
6 changes: 6 additions & 0 deletions examples/path.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@
exists=True,
message="Give me existing file",
),
inquirer.Path(
"existing_dir",
path_type=inquirer.Path.DIRECTORY,
exists=True,
message="Give me existing dir",
),
]

answers = inquirer.prompt(questions)
Expand Down
88 changes: 19 additions & 69 deletions src/inquirer/questions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@

from __future__ import annotations

import errno
import json
import os
import sys
import pathlib

import inquirer.errors as errors
from inquirer.render.console._other import GLOBAL_OTHER_CHOICE
Expand Down Expand Up @@ -187,53 +185,14 @@ def __init__(
self.carousel = carousel
self.autocomplete = autocomplete


# 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

if not os.path.isdir(root_dirname):
raise Exception("'%s' is not a directory.", 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):
def __init__(self, name, default=None, path_type="any", exists=None, **kwargs):
super().__init__(name, default=default, **kwargs)

if path_type in (Path.ANY, Path.FILE, Path.DIRECTORY):
Expand All @@ -242,56 +201,47 @@ def __init__(self, name, default=None, path_type="any", exists=None, normalize_t
raise ValueError("'path_type' must be one of [ANY, FILE, DIRECTORY]")

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):
def validate(self, current:str):
super().validate(current)

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

current = self.normalize_value(current)
path = pathlib.Path(current)

# this block validates the path in correspondence with the OS
# it will error if the path contains invalid characters
try:
path.lstat()
except FileNotFoundError:
pass
except (ValueError,OSError) as e:
raise errors.ValidationError(e)

if not is_pathname_valid(current):
if self._exists and not path.exists():
raise errors.ValidationError(current)

# os.path.isdir and isfile check also existence of the path,
# which might not be desirable
if self._path_type == Path.FILE:
if self._exists is None and os.path.basename(current) == "":
raise errors.ValidationError(current)
elif self._exists and not os.path.isfile(current):
if current.endswith(("\\", "/")):
raise errors.ValidationError(current)
elif self._exists is not None and not self._exists and os.path.isfile(current):
if path.exists() and not path.is_file():
raise errors.ValidationError(current)

elif self._path_type == Path.DIRECTORY:
if self._exists is None and os.path.basename(current) != "":
raise errors.ValidationError(current)
elif self._exists and not os.path.isdir(current):
if self._path_type == Path.DIRECTORY:
if current == "":
raise errors.ValidationError(current)
elif self._exists is not None and not self._exists and os.path.isdir(current):
if path.exists() and not path.is_dir():
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


def question_factory(kind, *args, **kwargs):
for cl in (Text, Editor, Password, Confirm, List, Checkbox, Path):
Expand Down
125 changes: 65 additions & 60 deletions tests/unit/test_question.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import os
import pathlib
import shutil
import tempfile
import unittest
import sys

from inquirer import errors
from inquirer import questions
Expand Down Expand Up @@ -247,13 +248,22 @@ def do_test(path, result=True):

do_test(None, False)

# Path component must not be longer then 255 bytes
do_test("a" * 256, False)
do_test(os.path.abspath("/asdf/" + "a" * 256), False)
do_test("{}/{}".format("a" * 255, "b" * 255), True)

# Path component must not contains null bytes
do_test("some/path/with/{}byte".format(b"\x00".decode("utf-8")), False)
do_test("some/path/with/{}byte".format("\x00"), False)

@unittest.skipUnless(sys.platform.startswith("lin"), "Linux only")
def test_path_validation_linux(self):
q = questions.Path("validation_test")
for path in ["foo\\bar"]:
with self.assertRaises(errors.ValidationError):
q.validate(path)

@unittest.skipUnless(sys.platform.startswith("win"), "Windows only")
def test_path_validation_linux(self):
q = questions.Path("validation_test")
for path in ["fo:/bar"]:
with self.assertRaises(errors.ValidationError):
q.validate(path)

def test_path_type_validation_no_existence_check(self):
def do_test(path_type, path, result=True):
Expand Down Expand Up @@ -281,75 +291,70 @@ def do_test(path_type, path, result=True):
do_test(questions.Path.FILE, "/aa/bb")
do_test(questions.Path.FILE, "~/aa/.bb")

do_test(questions.Path.DIRECTORY, "./aa/bb", False)
do_test(questions.Path.DIRECTORY, "./aa/bb")
do_test(questions.Path.DIRECTORY, "./aa/")
do_test(questions.Path.DIRECTORY, "aa/bb", False)
do_test(questions.Path.DIRECTORY, "aa/bb")
do_test(questions.Path.DIRECTORY, "aa/")
do_test(questions.Path.DIRECTORY, "/aa/")
do_test(questions.Path.DIRECTORY, "~/aa/")
do_test(questions.Path.DIRECTORY, "/aa/bb", False)
do_test(questions.Path.DIRECTORY, "~/aa/bb", False)

def test_path_type_validation_existing(self):
root = tempfile.mkdtemp()
some_existing_dir = os.path.join(root, "some_dir")
some_non_existing_dir = os.path.join(root, "some_non_existing_dir")
some_existing_file = os.path.join(root, "some_file")
some_non_existing_file = os.path.join(root, "some_non_existing_file")
do_test(questions.Path.DIRECTORY, "/aa/bb")
do_test(questions.Path.DIRECTORY, "~/aa/bb")

def test_path_type_validation_existence_check(self):
root = pathlib.Path(tempfile.mkdtemp())
some_non_existing_dir = root / "some_non_existing_dir"
some_existing_dir = root / "some_dir"
some_existing_dir.mkdir()
some_non_existing_file = root / "some_non_existing_file"
some_existing_file = root / "some_file"
some_existing_file.touch()

os.mkdir(some_existing_dir)
open(some_existing_file, "a").close()

def do_test(path_type, path, exists, result=True):
q = questions.Path("path_type_test", exists=exists, path_type=path_type)
def do_test(path_type, path, result=True):
q = questions.Path("path_type_test", exists=True, path_type=path_type)
if result:
self.assertIsNone(q.validate(path))
self.assertIsNone(q.validate(str(path)))
else:
with self.assertRaises(errors.ValidationError):
q.validate(path)
q.validate(str(path))

try:
do_test(questions.Path.ANY, some_existing_file, True, True)
do_test(questions.Path.ANY, some_non_existing_file, True, False)
do_test(questions.Path.ANY, some_existing_file, False, False)
do_test(questions.Path.ANY, some_non_existing_file, False, True)
do_test(questions.Path.ANY, some_existing_dir, True, True)
do_test(questions.Path.ANY, some_non_existing_dir, True, False)
do_test(questions.Path.ANY, some_existing_dir, False, False)
do_test(questions.Path.ANY, some_non_existing_dir, False, True)

do_test(questions.Path.FILE, some_existing_file, True, True)
do_test(questions.Path.FILE, some_non_existing_file, True, False)
do_test(questions.Path.FILE, some_non_existing_file, False, True)
do_test(questions.Path.FILE, some_existing_file, False, False)

do_test(questions.Path.DIRECTORY, some_existing_dir, True, True)
do_test(questions.Path.DIRECTORY, some_non_existing_dir, True, False)
do_test(questions.Path.DIRECTORY, some_existing_dir, False, False)
do_test(questions.Path.DIRECTORY, some_non_existing_dir, False, True)
do_test(questions.Path.ANY, some_existing_file)
do_test(questions.Path.ANY, some_non_existing_file, False)
do_test(questions.Path.ANY, some_existing_dir)
do_test(questions.Path.ANY, some_non_existing_dir, False)

finally:
shutil.rmtree(root)
do_test(questions.Path.FILE, some_existing_file)
do_test(questions.Path.FILE, some_non_existing_file, False)
do_test(questions.Path.FILE, some_existing_dir, False)

def test_normalizing_value(self):
# Expanding Home
home = os.path.expanduser("~")
q = questions.Path("home")
do_test(questions.Path.DIRECTORY, some_existing_dir)
do_test(questions.Path.DIRECTORY, some_non_existing_dir, False)
do_test(questions.Path.DIRECTORY, some_existing_file, False)

path = "~/some_path/some_file"
self.assertNotIn(home, path)
self.assertIn(home, q.normalize_value(path))

# Normalizing to absolute path
root = os.path.abspath(__file__).split(os.path.sep)[0]
q = questions.Path("abs_path", normalize_to_absolute_path=True)
self.assertEqual(root, q.normalize_value("some/relative/path").split(os.path.sep)[0])
finally:
shutil.rmtree(root)

def test_default_value_validation(self):
with self.assertRaises(ValueError):
questions.Path("path", default="~/.toggl_log", path_type=questions.Path.DIRECTORY)
root = pathlib.Path(tempfile.mkdtemp())
some_non_existing_dir = root / "some_non_existing_dir"
some_existing_dir = root / "some_dir"
some_existing_dir.mkdir()
some_non_existing_file = root / "some_non_existing_file"
some_existing_file = root / "some_file"
some_existing_file.touch()

def do_test(default, path_type, exists, result=True):
if result:
questions.Path("path", default=str(default), exists=exists, path_type=path_type)
else:
with self.assertRaises(ValueError):
questions.Path("path", default=str(default), exists=exists, path_type=path_type)

do_test(some_existing_dir, questions.Path.DIRECTORY, exists=True)
do_test(some_non_existing_dir, questions.Path.DIRECTORY, exists=True, result=False)

questions.Path("path", default="~/.toggl_log")
do_test(some_existing_file, questions.Path.FILE, exists=True)
do_test(some_non_existing_file, questions.Path.FILE, exists=True, result=False)

def test_path_type_value_validation(self):
questions.Path("abs_path", path_type=questions.Path.ANY)
Expand Down

0 comments on commit ed5aa4b

Please sign in to comment.