Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix dot-files validation #60

Merged
merged 8 commits into from
Jan 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:

- uses: actions/setup-python@v5
with:
python-version: "3.12"
python-version: "3.13"
cache: pip
cache-dependency-path: |
setup.py
Expand Down Expand Up @@ -62,7 +62,7 @@ jobs:

- uses: actions/setup-python@v5
with:
python-version: "3.12"
python-version: "3.13"
cache: pip
cache-dependency-path: |
setup.py
Expand All @@ -88,7 +88,7 @@ jobs:

- uses: actions/setup-python@v5
with:
python-version: "3.12"
python-version: "3.13"
cache: pip
cache-dependency-path: |
setup.py
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/on_push_default_branch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:

- uses: actions/setup-python@v5
with:
python-version: "3.12"
python-version: "3.13"
cache: pip
cache-dependency-path: |
setup.py
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:

- uses: actions/setup-python@v5
with:
python-version: "3.12"
python-version: "3.13"
cache: pip
cache-dependency-path: |
setup.py
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ LAST_UPDATE_YEAR := $(shell git log -1 --format=%cd --date=format:%Y)


$(BIN_CHANGELOG_FROM_RELEASE):
mkdir -p $(BIN_DIR)
GOBIN=$(BIN_DIR) go install github.com/rhysd/changelog-from-release/v3@latest

.PHONY: build
Expand Down
1 change: 1 addition & 0 deletions docs/pages/reference/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ Reference
function
types
handler
tips
53 changes: 53 additions & 0 deletions docs/pages/reference/tips.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
Tips
------------

Sanitize dot-files or dot-directories
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
When you process filenames or filepaths containing ``.`` or ``..`` with the ``sanitize_filename`` function or the ``sanitize_filepath`` function, by default, ``sanitize_filename`` does nothing, and ``sanitize_filepath`` normalizes the filepaths:

.. code-block:: python

print(sanitize_filename("."))
print(sanitize_filepath("hoge/./foo"))

.. code-block:: console

.
hoge/foo

If you would like to replace ``.`` and ``..`` like other reserved words, you need to specify the arguments as follows:

.. code-block:: python

from pathvalidate import sanitize_filepath, sanitize_filename
from pathvalidate.error import ValidationError


def always_add_trailing_underscore(e: ValidationError) -> str:
if e.reusable_name:
return e.reserved_name

return f"{e.reserved_name}_"


print(
sanitize_filename(
".",
reserved_name_handler=always_add_trailing_underscore,
additional_reserved_names=[".", ".."],
)
)

print(
sanitize_filepath(
"hoge/./foo",
normalize=False,
reserved_name_handler=always_add_trailing_underscore,
additional_reserved_names=[".", ".."],
)
)

.. code-block:: console

._
hoge/._/foo
30 changes: 18 additions & 12 deletions pathvalidate/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ def is_valid(self, value: PathType) -> bool:
return True

def _is_reserved_keyword(self, value: str) -> bool:
return value in self.reserved_keywords
return value.upper() in self.reserved_keywords


class AbstractSanitizer(BaseFile, metaclass=abc.ABCMeta):
Expand Down Expand Up @@ -183,6 +183,7 @@ def sanitize(self, value: PathType, replacement_text: str = "") -> PathType: #

class BaseValidator(AbstractValidator):
__RE_ROOT_NAME: Final = re.compile(r"([^\.]+)")
__RE_REPEAD_DOT: Final = re.compile(r"^\.{3,}")

@property
def min_len(self) -> int:
Expand Down Expand Up @@ -218,17 +219,16 @@ def _validate_reserved_keywords(self, name: str) -> None:
return

root_name = self.__extract_root_name(name)
base_name = os.path.basename(name).upper()

if self._is_reserved_keyword(root_name.upper()) or self._is_reserved_keyword(
base_name.upper()
):
raise ReservedNameError(
f"'{root_name}' is a reserved name",
reusable_name=False,
reserved_name=root_name,
platform=self.platform,
)
base_name = os.path.basename(name)

for name in (root_name, base_name):
if self._is_reserved_keyword(name):
raise ReservedNameError(
f"'{root_name}' is a reserved name",
reusable_name=False,
reserved_name=root_name,
platform=self.platform,
)

def _validate_max_len(self) -> None:
if self.max_len < 1:
Expand All @@ -239,6 +239,12 @@ def _validate_max_len(self) -> None:

@classmethod
def __extract_root_name(cls, path: str) -> str:
if path in (".", ".."):
return path

if cls.__RE_REPEAD_DOT.search(path):
return path

match = cls.__RE_ROOT_NAME.match(os.path.basename(path))
if match is None:
return ""
Expand Down
8 changes: 4 additions & 4 deletions pathvalidate/_filename.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,8 +302,8 @@ def validate_filename(

Defaults to ``255``.
fs_encoding:
Filesystem encoding that used to calculate the byte length of the filename.
If |None|, get the value from the execution environment.
Filesystem encoding that is used to calculate the byte length of the filename.
If |None|, get the encoding from the execution environment.
check_reserved:
If |True|, check the reserved names of the ``platform``.
additional_reserved_names:
Expand Down Expand Up @@ -414,8 +414,8 @@ def sanitize_filename(
Truncate the name length if the ``filename`` length exceeds this value.
Defaults to ``255``.
fs_encoding:
Filesystem encoding that used to calculate the byte length of the filename.
If |None|, get the value from the execution environment.
Filesystem encoding that is used to calculate the byte length of the filename.
If |None|, get the encoding from the execution environment.
check_reserved:
[Deprecated] Use 'reserved_name_handler' instead.
null_value_handler:
Expand Down
8 changes: 4 additions & 4 deletions pathvalidate/_filepath.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,8 +337,8 @@ def validate_filepath(
- ``Windows``: 260
- ``universal``: 260
fs_encoding (Optional[str], optional):
Filesystem encoding that used to calculate the byte length of the file path.
If |None|, get the value from the execution environment.
Filesystem encoding that is used to calculate the byte length of the file path.
If |None|, get the encoding from the execution environment.
check_reserved (bool, optional):
If |True|, check the reserved names of the ``platform``.
Defaults to |True|.
Expand Down Expand Up @@ -455,8 +455,8 @@ def sanitize_filepath(
- ``Windows``: 260
- ``universal``: 260
fs_encoding:
Filesystem encoding that used to calculate the byte length of the file path.
If |None|, get the value from the execution environment.
Filesystem encoding that is used to calculate the byte length of the file path.
If |None|, get the encoding from the execution environment.
check_reserved:
[Deprecated] Use 'reserved_name_handler' instead.
null_value_handler:
Expand Down
34 changes: 33 additions & 1 deletion test/test_filename.py
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,8 @@ def test_reserved_name(self, value, platform, expected):
["Abc", ["abc"], False],
["ABC", ["abc"], False],
["abc.txt", ["abc.txt"], False],
[".", [".", ".."], False],
["..", [".", ".."], False],
],
)
def test_normal_additional_reserved_names(self, value, arn, expected):
Expand Down Expand Up @@ -654,6 +656,35 @@ def test_normal_reserved_name_handler(self, value, reserved_name_handler, expect
== expected
)

@pytest.mark.parametrize(
["value", "test_platform", "arn", "expected"],
[
[".", "windows", [".", ".."], "._"],
[".", "universal", [".", ".."], "._"],
["..", "windows", [".", ".."], ".._"],
["..", "universal", [".", ".."], ".._"],
["...", "linux", [".", ".."], "..."],
],
)
def test_normal_custom_reserved_name_handler_for_dot_files(
self, value, test_platform, arn, expected
):
def always_add_trailing_underscore(e: ValidationError) -> str:
if e.reusable_name:
return e.reserved_name

return f"{e.reserved_name}_"

assert (
sanitize_filename(
value,
platform=test_platform,
reserved_name_handler=always_add_trailing_underscore,
additional_reserved_names=arn,
)
== expected
)

def test_exception_reserved_name_handler(self):
for platform in ["windows", "universal"]:
with pytest.raises(ValidationError) as e:
Expand All @@ -676,7 +707,7 @@ def test_normal_additional_reserved_names(self, value, arn, expected):
additional_reserved_names=arn,
)
== expected
)
), platform

@pytest.mark.parametrize(
["value", "check_reserved", "expected"],
Expand Down Expand Up @@ -709,6 +740,7 @@ def test_normal_check_reserved(self, value, check_reserved, expected):
["linux", "period.", "period."],
["linux", "space ", "space "],
["linux", "space_and_period. ", "space_and_period. "],
["linux", "...", "..."],
["universal", "period.", "period"],
["universal", "space ", "space"],
["universal", "space_and_period .", "space_and_period"],
Expand Down
28 changes: 28 additions & 0 deletions test/test_filepath.py
Original file line number Diff line number Diff line change
Expand Up @@ -701,7 +701,9 @@ def test_normal_reserved_name(self, value, test_platform, expected):
["value", "reserved_name_handler", "expected"],
[
["CON", ReservedNameHandler.add_trailing_underscore, "CON_"],
["hoge/CON", ReservedNameHandler.add_trailing_underscore, "hoge\\CON_"],
["CON", ReservedNameHandler.add_leading_underscore, "_CON"],
["hoge/CON", ReservedNameHandler.add_leading_underscore, "hoge\\_CON"],
["CON", ReservedNameHandler.as_is, "CON"],
],
)
Expand All @@ -713,6 +715,32 @@ def test_normal_reserved_name_handler(self, value, reserved_name_handler, expect
== expected
)

@pytest.mark.parametrize(
["value", "expected"],
[
["hoge/.", "hoge\\._"],
["hoge/./foo", "hoge\\._\\foo"],
["hoge/..", "hoge\\.._"],
],
)
def test_normal_custom_reserved_name_handler_for_dot_files(self, value, expected):
def always_add_trailing_underscore(e: ValidationError) -> str:
if e.reusable_name:
return e.reserved_name

return f"{e.reserved_name}_"

assert (
sanitize_filepath(
value,
platform="windows",
reserved_name_handler=always_add_trailing_underscore,
additional_reserved_names=[".", ".."],
normalize=False,
)
== expected
)

def test_exception_reserved_name_handler(self):
for platform in ["windows", "universal"]:
with pytest.raises(ValidationError) as e:
Expand Down
Loading