pax_global_header00006660000000000000000000000064145014660450014517gustar00rootroot0000000000000052 comment=cc4392263eeaeb76530d57907f95721b8af85f08 pathvalidate-3.2.0/000077500000000000000000000000001450146604500141675ustar00rootroot00000000000000pathvalidate-3.2.0/.github/000077500000000000000000000000001450146604500155275ustar00rootroot00000000000000pathvalidate-3.2.0/.github/FUNDING.yml000066400000000000000000000011421450146604500173420ustar00rootroot00000000000000# These are supported funding model platforms github: thombashi patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: thombashi issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] pathvalidate-3.2.0/.github/workflows/000077500000000000000000000000001450146604500175645ustar00rootroot00000000000000pathvalidate-3.2.0/.github/workflows/tests.yml000066400000000000000000000077301450146604500214600ustar00rootroot00000000000000name: CI on: push: paths-ignore: - ".gitignore" - "README.rst" pull_request: paths-ignore: - ".gitignore" - "README.rst" jobs: build-package: runs-on: ubuntu-latest concurrency: group: ${{ github.event_name }}-${{ github.workflow }}-${{ github.ref_name }}-build cancel-in-progress: true timeout-minutes: 20 container: image: ghcr.io/thombashi/python-ci:3.11 steps: - uses: actions/checkout@v4 - run: make build lint: runs-on: ubuntu-latest concurrency: group: ${{ github.event_name }}-${{ github.workflow }}-${{ github.ref_name }}-lint cancel-in-progress: true timeout-minutes: 20 container: image: ghcr.io/thombashi/python-ci:3.11 steps: - uses: actions/checkout@v4 - run: make check unit-test: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "pypy-3.10"] os: [ubuntu-latest, macos-latest, windows-latest] exclude: - os: windows-latest python-version: "3.11" concurrency: group: ${{ github.event_name }}-${{ github.workflow }}-unit-test-${{ matrix.os }}-${{ matrix.python-version }} cancel-in-progress: true timeout-minutes: 20 steps: - uses: actions/checkout@v4 - name: Setup Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} cache: pip cache-dependency-path: | setup.py **/*requirements.txt tox.ini - name: Install pip run: python -m pip install --upgrade --disable-pip-version-check "pip>=21.1" - run: make setup-ci - name: Run tests env: PYTEST_DISCORD_WEBHOOK: ${{ secrets.PYTEST_DISCORD_WEBHOOK }} run: tox -e py run-coverage: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: python-version: ["3.10"] os: [ubuntu-latest, macos-latest, windows-latest] concurrency: group: ${{ github.event_name }}-${{ github.workflow }}-run-coverage-${{ matrix.os }}-${{ matrix.python-version }} cancel-in-progress: true timeout-minutes: 20 defaults: run: shell: bash steps: - uses: actions/checkout@v4 - name: Setup Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} cache: pip cache-dependency-path: | setup.py **/*requirements.txt tox.ini - name: Setup env run: echo "COVERAGE_FILE=.coverage_${{ matrix.os }}_${{ matrix.python-version }} " >> $GITHUB_ENV - run: make setup-ci - name: Run tests run: tox -e cov - run: ls -a - uses: actions/upload-artifact@v3 with: name: coverage path: .coverage_* upload-coverage: needs: run-coverage runs-on: ubuntu-latest timeout-minutes: 20 steps: - uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v4 with: python-version: "3.11" cache: pip cache-dependency-path: | setup.py **/*requirements.txt tox.ini - uses: actions/download-artifact@v3 with: name: coverage path: artifact - name: List artifacts working-directory: artifact run: | set -x pwd ls -alR - name: Install packages run: python -m pip install --upgrade --disable-pip-version-check coveralls tomli - name: Combine coverage reports run: | coverage combine artifact/.coverage_* ls -alR - name: Upload coverage report env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: coveralls --service=github pathvalidate-3.2.0/.gitignore000066400000000000000000000026001450146604500161550ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # User settings _sandbox/ *_profile Untitled.ipynb .pytype pathvalidate-3.2.0/.readthedocs.yaml000066400000000000000000000003311450146604500174130ustar00rootroot00000000000000version: 2 build: os: ubuntu-22.04 tools: python: "3.11" sphinx: configuration: docs/conf.py formats: - epub python: install: - method: pip path: . extra_requirements: - docs pathvalidate-3.2.0/LICENSE000066400000000000000000000020741450146604500151770ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2016 Tsuyoshi Hombashi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pathvalidate-3.2.0/MANIFEST.in000066400000000000000000000003621450146604500157260ustar00rootroot00000000000000include LICENSE include README.rst include setup.cfg include tox.ini include docs/pages/introduction/summary.txt include */py.typed recursive-include test * recursive-include requirements * global-exclude __pycache__/* global-exclude *.pyc pathvalidate-3.2.0/Makefile000066400000000000000000000023601450146604500156300ustar00rootroot00000000000000AUTHOR := thombashi PACKAGE := pathvalidate BUILD_WORK_DIR := _work PKG_BUILD_DIR := $(BUILD_WORK_DIR)/$(PACKAGE) PYTHON := python3 .PHONY: build-remote build-remote: clean @mkdir -p $(BUILD_WORK_DIR) @cd $(BUILD_WORK_DIR) && \ git clone https://github.com/$(AUTHOR)/$(PACKAGE).git --depth 1 && \ cd $(PACKAGE) && \ $(PYTHON) -m tox -e build ls -lh $(PKG_BUILD_DIR)/dist/* .PHONY: build build: clean @$(PYTHON) -m tox -e build ls -lh dist/* .PHONY: check check: $(PYTHON) -m tox -e lint -$(PYTHON) -m tox -e lint-examples -rm examples/pathvalidate_examples.py .PHONY: clean clean: @rm -rf $(BUILD_WORK_DIR) @$(PYTHON) -m tox -e clean .PHONY: docs docs: @$(PYTHON) -m tox -e docs .PHONY: idocs idocs: @$(PYTHON) -m pip install -q --disable-pip-version-check --upgrade -e . @$(MAKE) docs .PHONY: fmt fmt: @$(PYTHON) -m tox -e fmt .PHONY: readme readme: @$(PYTHON) -m tox -e readme .PHONY: release release: cd $(PKG_BUILD_DIR) && $(PYTHON) setup.py release --sign --verbose $(MAKE) clean .PHONY: setup-ci setup-ci: @$(PYTHON) -m pip install -q --disable-pip-version-check --upgrade tox .PHONY: setup setup: setup-ci @$(PYTHON) -m pip install -q --disable-pip-version-check --upgrade -e .[test] releasecmd @$(PYTHON) -m pip check pathvalidate-3.2.0/README.rst000066400000000000000000000225151450146604500156630ustar00rootroot00000000000000.. contents:: **pathvalidate** :backlinks: top :depth: 2 Summary ========= `pathvalidate `__ is a Python library to sanitize/validate a string such as filenames/file-paths/etc. .. image:: https://badge.fury.io/py/pathvalidate.svg :target: https://badge.fury.io/py/pathvalidate :alt: PyPI package version .. image:: https://anaconda.org/thombashi/pathvalidate/badges/version.svg :target: https://anaconda.org/thombashi/pathvalidate :alt: conda package version .. image:: https://img.shields.io/pypi/pyversions/pathvalidate.svg :target: https://pypi.org/project/pathvalidate :alt: Supported Python versions .. image:: https://img.shields.io/pypi/implementation/pathvalidate.svg :target: https://pypi.org/project/pathvalidate :alt: Supported Python implementations .. image:: https://github.com/thombashi/pathvalidate/workflows/Tests/badge.svg :target: https://github.com/thombashi/pathvalidate/actions?query=workflow%3ATests :alt: Linux/macOS/Windows CI status .. image:: https://coveralls.io/repos/github/thombashi/pathvalidate/badge.svg?branch=master :target: https://coveralls.io/github/thombashi/pathvalidate?branch=master :alt: Test coverage: coveralls .. image:: https://github.com/thombashi/pathvalidate/actions/workflows/github-code-scanning/codeql/badge.svg :target: https://github.com/thombashi/pathvalidate/actions/workflows/github-code-scanning/codeql :alt: CodeQL Features --------- - Sanitize/Validate a string as a: - file name - file path - Sanitize will do: - Remove invalid characters for a target platform - Replace reserved names for a target platform - Normalize - Remove unprintable characters - Argument validator/sanitizer for ``argparse`` and ``click`` - Multi platform support: - ``Linux`` - ``Windows`` - ``macOS`` - ``POSIX`` - ``universal`` (platform independent) - Multibyte character support Examples ========== Sanitize a filename --------------------- :Sample Code: .. code-block:: python from pathvalidate import sanitize_filename fname = "fi:l*e/p\"a?t>h|.t {sanitize_filename(fname)}\n") fname = "\0_a*b:ce%f/(g)h+i_0.txt" print(f"{fname} -> {sanitize_filename(fname)}\n") :Output: .. code-block:: fi:l*e/p"a?t>h|.t filepath.txt _a*b:ce%f/(g)h+i_0.txt -> _abcde%f(g)h+i_0.txt The default target ``platform`` is ``universal``. i.e. the sanitized file name is valid for any platform. Sanitize a filepath --------------------- :Sample Code: .. code-block:: python from pathvalidate import sanitize_filepath fpath = "fi:l*e/p\"a?t>h|.t {sanitize_filepath(fpath)}\n") fpath = "\0_a*b:ce%f/(g)h+i_0.txt" print(f"{fpath} -> {sanitize_filepath(fpath)}\n") :Output: .. code-block:: fi:l*e/p"a?t>h|.t file/path.txt _a*b:ce%f/(g)h+i_0.txt -> _abcde%f/(g)h+i_0.txt Validate a filename --------------------- :Sample Code: .. code-block:: python import sys from pathvalidate import ValidationError, validate_filename try: validate_filename("fi:l*e/p\"a?t>h|.th|.th|.t None: if filename: click.echo(f"filename: {filename}") if filepath: click.echo(f"filepath: {filepath}") if __name__ == "__main__": cli() :Output: .. code-block:: $ ./examples/click_validate.py --filename ab filename: ab $ ./examples/click_validate.py --filepath e?g Usage: click_validate.py [OPTIONS] Try 'click_validate.py --help' for help. Error: Invalid value for '--filename': [PV1100] invalid characters found: invalids=('?'), value='e?g', platform=Windows filename/filepath sanitizer for ``click`` ------------------------------------------- :Sample Code: .. code-block:: python import click from pathvalidate.click import sanitize_filename_arg, sanitize_filepath_arg @click.command() @click.option("--filename", callback=sanitize_filename_arg) @click.option("--filepath", callback=sanitize_filepath_arg) def cli(filename, filepath): if filename: click.echo(f"filename: {filename}") if filepath: click.echo(f"filepath: {filepath}") if __name__ == "__main__": cli() :Output: .. code-block:: $ ./examples/click_sanitize.py --filename a/b filename: ab For more information ---------------------- More examples can be found at https://pathvalidate.rtfd.io/en/latest/pages/examples/index.html Installation ============ Installation: pip ------------------------------ :: pip install pathvalidate Installation: conda ------------------------------ :: conda install -c thombashi pathvalidate Installation: apt ------------------------------ :: sudo add-apt-repository ppa:thombashi/ppa sudo apt update sudo apt install python3-pathvalidate Dependencies ============ Python 3.7+ no external dependencies. Documentation =============== https://pathvalidate.rtfd.io/ Sponsors ==================================== .. image:: https://avatars.githubusercontent.com/u/44389260?s=48&u=6da7176e51ae2654bcfd22564772ef8a3bb22318&v=4 :target: https://github.com/chasbecker :alt: Charles Becker (chasbecker) .. image:: https://avatars.githubusercontent.com/u/9919?s=48&v=4 :target: https://github.com/github :alt: onetime: GitHub (github) .. image:: https://avatars.githubusercontent.com/u/46711571?s=48&u=57687c0e02d5d6e8eeaf9177f7b7af4c9f275eb5&v=4 :target: https://github.com/Arturi0 :alt: onetime: Arturi0 .. image:: https://avatars.githubusercontent.com/u/3658062?s=48&v=4 :target: https://github.com/b4tman :alt: onetime: Dmitry Belyaev (b4tman) `Become a sponsor `__ pathvalidate-3.2.0/docs/000077500000000000000000000000001450146604500151175ustar00rootroot00000000000000pathvalidate-3.2.0/docs/Makefile000066400000000000000000000176301450146604500165660ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don\'t have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " applehelp to make an Apple Help Book" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " epub3 to make an epub3" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" @echo " dummy to check syntax errors of document sources" .PHONY: clean clean: rm -rf $(BUILDDIR)/* .PHONY: html html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." .PHONY: dirhtml dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." .PHONY: singlehtml singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." .PHONY: pickle pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." .PHONY: json json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." .PHONY: htmlhelp htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." .PHONY: qthelp qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pathvalidate.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pathvalidate.qhc" .PHONY: applehelp applehelp: $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp @echo @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." @echo "N.B. You won't be able to view it unless you put it in" \ "~/Library/Documentation/Help or install it in your application" \ "bundle." .PHONY: devhelp devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/pathvalidate" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pathvalidate" @echo "# devhelp" .PHONY: epub epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." .PHONY: epub3 epub3: $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 @echo @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." .PHONY: latex latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." .PHONY: latexpdf latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." .PHONY: latexpdfja latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." .PHONY: text text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." .PHONY: man man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." .PHONY: texinfo texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." .PHONY: info info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." .PHONY: gettext gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." .PHONY: changes changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." .PHONY: linkcheck linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." .PHONY: doctest doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." .PHONY: coverage coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." .PHONY: xml xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." .PHONY: pseudoxml pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." .PHONY: dummy dummy: $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy @echo @echo "Build finished. Dummy builder generates no files." pathvalidate-3.2.0/docs/conf.py000066400000000000000000000235211450146604500164210ustar00rootroot00000000000000import os import sys import sphinx_rtd_theme from pathvalidate import __author__, __copyright__, __name__, __version__ # 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. sys.path.insert(0, os.path.abspath('../pathvalidate')) # -- 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', 'sphinx.ext.todo', 'sphinx.ext.viewcode', 'sphinx.ext.napoleon', ] intersphinx_mapping = {'python': ('https://docs.python.org/', None)} # 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 encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = __name__ copyright = __copyright__ author = __author__ # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = __version__ # The full version, including alpha/beta/rc tags. release = version # 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 = 'en' # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # The reST default role (used for this markup: `text`) to use for all # documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # -- 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 themes here, relative to this directory. html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # The name for this set of Sphinx documents. # " v documentation" by default. #html_title = u'pathvalidate v0.1.0' # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (relative to this directory) to use as a favicon of # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # 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'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. #html_extra_path = [] # If not None, a 'Last updated on:' timestamp is inserted at every page # bottom, using the given strftime format. # The empty string is equivalent to '%b %d, %Y'. #html_last_updated_fmt = None # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' #html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # 'ja' uses this config value. # 'zh' user can custom change `jieba` dictionary path. #html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. #html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = 'pathvalidatedoc' # -- 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, 'pathvalidate.tex', 'pathvalidate Documentation', __author__, 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- 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, 'pathvalidate', 'pathvalidate Documentation', [author], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- 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, 'pathvalidate', 'pathvalidate Documentation', author, 'pathvalidate', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False rst_prolog = r""" .. |False| replace:: :py:obj:`False` .. |True| replace:: :py:obj:`True` .. |None| replace:: :py:obj:`None` .. |invalid_file_path_chars| replace:: ``\\0`` .. |invalid_win_file_path_chars| replace:: ``:``, ``*``, ``?``, ``"``, ``<``, ``>``, ``|``, ``\t``, ``\n``, ``\r``, ``\x0b``, ``\x0c`` .. |invalid_filename_chars| replace:: ``/``, ``\\0`` .. |invalid_win_filename_chars| replace:: ``\``, ``:``, ``*``, ``?``, ``"``, ``<``, ``>``, ``|``, ``\t``, ``\n``, ``\r``, ``\x0b``, ``\x0c`` .. |invalid_excel_sheet_chars| replace:: ``[``, ``]``, ``:``, ``*``, ``?``, ``/``, ``\`` .. |raises_sqlite_keywords| replace:: If the ``name`` is equals to `SQLite Keywords `__. """ pathvalidate-3.2.0/docs/index.rst000066400000000000000000000010721450146604500167600ustar00rootroot00000000000000Welcome to pathvalidate's documentation! ======================================== .. raw:: html

.. toctree:: :caption: Table of Contents :maxdepth: 3 :numbered: pages/introduction/index pages/examples/index pages/reference/index pages/links Indices and tables ================== * :ref:`genindex` pathvalidate-3.2.0/docs/make.bat000066400000000000000000000164471450146604500165400ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. epub3 to make an epub3 echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. xml to make Docutils-native XML files echo. pseudoxml to make pseudoxml-XML files for display purposes echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled echo. coverage to run coverage check of the documentation if enabled echo. dummy to check syntax errors of document sources goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) REM Check if sphinx-build is available and fallback to Python version if any %SPHINXBUILD% 1>NUL 2>NUL if errorlevel 9009 goto sphinx_python goto sphinx_ok :sphinx_python set SPHINXBUILD=python -m sphinx.__init__ %SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) :sphinx_ok if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\pathvalidate.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\pathvalidate.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "epub3" ( %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %~dp0 echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %~dp0 echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) if "%1" == "coverage" ( %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage if errorlevel 1 exit /b 1 echo. echo.Testing of coverage in the sources finished, look at the ^ results in %BUILDDIR%/coverage/python.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%/xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. goto end ) if "%1" == "dummy" ( %SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy if errorlevel 1 exit /b 1 echo. echo.Build finished. Dummy builder generates no files. goto end ) :end pathvalidate-3.2.0/docs/make_readme.py000066400000000000000000000045571450146604500177360ustar00rootroot00000000000000#!/usr/bin/env python3 """ .. codeauthor:: Tsuyoshi Hombashi """ import sys from path import Path from readmemaker import ReadmeMaker PROJECT_NAME = "pathvalidate" OUTPUT_DIR = ".." def write_examples(maker: ReadmeMaker) -> None: maker.set_indent_level(0) maker.write_chapter("Examples") example_root = Path("pages").joinpath("examples") maker.inc_indent_level() maker.write_chapter("Sanitize a filename") maker.write_file(example_root.joinpath("sanitize_filename_code.txt")) maker.write_chapter("Sanitize a filepath") maker.write_file(example_root.joinpath("sanitize_filepath_code.txt")) maker.write_chapter("Validate a filename") maker.write_file(example_root.joinpath("validate_filename_code.txt")) maker.write_chapter("Check a filename") maker.write_file(example_root.joinpath("is_valid_filename_code.txt")) maker.write_chapter("filename/filepath validator for ``argparse``") maker.write_file(example_root.joinpath("argparse_validator.txt")) maker.write_chapter("filename/filepath sanitizer for ``argparse``") maker.write_file(example_root.joinpath("argparse_sanitizer.txt")) maker.write_chapter("filename/filepath validator for ``click``") maker.write_file(example_root.joinpath("click_validator.txt")) maker.write_chapter("filename/filepath sanitizer for ``click``") maker.write_file(example_root.joinpath("click_sanitizer.txt")) maker.write_chapter("For more information") maker.write_lines( [ "More examples can be found at ", f"https://{PROJECT_NAME}.rtfd.io/en/latest/pages/examples/index.html", ] ) def main() -> int: maker = ReadmeMaker( PROJECT_NAME, OUTPUT_DIR, is_make_toc=True, project_url=f"https://github.com/thombashi/{PROJECT_NAME}", ) maker.write_chapter("Summary") maker.write_introduction_file("summary.txt") maker.write_introduction_file("badges.txt") maker.write_introduction_file("feature.txt") write_examples(maker) maker.write_introduction_file("installation.rst") maker.set_indent_level(0) maker.write_chapter("Documentation") maker.write_lines([f"https://{PROJECT_NAME}.rtfd.io/"]) maker.write_file(maker.doc_page_root_dir_path.joinpath("sponsors.rst")) return 0 if __name__ == "__main__": sys.exit(main()) pathvalidate-3.2.0/docs/pages/000077500000000000000000000000001450146604500162165ustar00rootroot00000000000000pathvalidate-3.2.0/docs/pages/examples/000077500000000000000000000000001450146604500200345ustar00rootroot00000000000000pathvalidate-3.2.0/docs/pages/examples/argparse.rst000066400000000000000000000004151450146604500223720ustar00rootroot00000000000000filename/filepath validator for argparse -------------------------------------------------------- .. include:: argparse_validator.txt filename/filepath sanitizer for argparse -------------------------------------------------------- .. include:: argparse_sanitizer.txt pathvalidate-3.2.0/docs/pages/examples/argparse_sanitizer.txt000066400000000000000000000013601450146604500244710ustar00rootroot00000000000000:Sample Code: .. code-block:: python from argparse import ArgumentParser from pathvalidate.argparse import sanitize_filename_arg, sanitize_filepath_arg parser = ArgumentParser() parser.add_argument("--filename", type=sanitize_filename_arg) parser.add_argument("--filepath", type=sanitize_filepath_arg) options = parser.parse_args() if options.filename: print("filename: {}".format(options.filename)) if options.filepath: print("filepath: {}".format(options.filepath)) :Output: .. code-block:: none $ ./examples/argparse_sanitize.py --filename e/g filename: eg .. note:: ``sanitize_filepath_arg`` is set platform as ``"auto"``. pathvalidate-3.2.0/docs/pages/examples/argparse_validator.txt000066400000000000000000000020461450146604500244500ustar00rootroot00000000000000:Sample Code: .. code-block:: python from argparse import ArgumentParser from pathvalidate.argparse import validate_filename_arg, validate_filepath_arg parser = ArgumentParser() parser.add_argument("--filename", type=validate_filename_arg) parser.add_argument("--filepath", type=validate_filepath_arg) options = parser.parse_args() if options.filename: print(f"filename: {options.filename}") if options.filepath: print(f"filepath: {options.filepath}") :Output: .. code-block:: none $ ./examples/argparse_validate.py --filename eg filename: eg $ ./examples/argparse_validate.py --filename e?g usage: argparse_validate.py [-h] [--filename FILENAME] [--filepath FILEPATH] argparse_validate.py: error: argument --filename: [PV1100] invalid characters found: invalids=(':'), value='e:g', platform=Windows .. note:: ``validate_filepath_arg`` consider ``platform`` as of ``"auto"`` if the input is an absolute file path. pathvalidate-3.2.0/docs/pages/examples/click.rst000066400000000000000000000004121450146604500216500ustar00rootroot00000000000000filename/filepath validator for ``click`` -------------------------------------------------------- .. include:: click_validator.txt filename/filepath sanitizer for ``click`` -------------------------------------------------------- .. include:: click_sanitizer.txt pathvalidate-3.2.0/docs/pages/examples/click_sanitizer.txt000066400000000000000000000012341450146604500237520ustar00rootroot00000000000000:Sample Code: .. code-block:: python import click from pathvalidate.click import sanitize_filename_arg, sanitize_filepath_arg @click.command() @click.option("--filename", callback=sanitize_filename_arg) @click.option("--filepath", callback=sanitize_filepath_arg) def cli(filename, filepath): if filename: click.echo(f"filename: {filename}") if filepath: click.echo(f"filepath: {filepath}") if __name__ == "__main__": cli() :Output: .. code-block:: none $ ./examples/click_sanitize.py --filename a/b filename: ab pathvalidate-3.2.0/docs/pages/examples/click_validator.txt000066400000000000000000000017001450146604500237250ustar00rootroot00000000000000:Sample Code: .. code-block:: python import click from pathvalidate.click import validate_filename_arg, validate_filepath_arg @click.command() @click.option("--filename", callback=validate_filename_arg) @click.option("--filepath", callback=validate_filepath_arg) def cli(filename: str, filepath: str) -> None: if filename: click.echo(f"filename: {filename}") if filepath: click.echo(f"filepath: {filepath}") if __name__ == "__main__": cli() :Output: .. code-block:: none $ ./examples/click_validate.py --filename ab filename: ab $ ./examples/click_validate.py --filepath e?g Usage: click_validate.py [OPTIONS] Try 'click_validate.py --help' for help. Error: Invalid value for '--filename': [PV1100] invalid characters found: invalids=('?'), value='e?g', platform=Windows pathvalidate-3.2.0/docs/pages/examples/index.rst000066400000000000000000000001521450146604500216730ustar00rootroot00000000000000Examples ======== .. toctree:: :maxdepth: 3 sanitize validate is_valid argparse click pathvalidate-3.2.0/docs/pages/examples/is_valid.rst000066400000000000000000000007031450146604500223600ustar00rootroot00000000000000.. _example-is-valid-filename: Check a filename ---------------------------- :py:func:`.is_valid_filename()` function returns |True| if a filename is valid for a specified platform. .. include:: is_valid_filename_code.txt .. _example-is-valid-filepath: Check a filepath ---------------------------- :py:func:`.is_valid_filepath()` function returns |True| if a filepath is valid for a specified platform. .. include:: is_valid_filepath_code.txt pathvalidate-3.2.0/docs/pages/examples/is_valid_filename_code.txt000066400000000000000000000010411450146604500252150ustar00rootroot00000000000000:Sample Code: .. code-block:: python from pathvalidate import is_valid_filename, sanitize_filename fname = "fi:l*e/p\"a?t>h|.th|.th|.th|.th|.t {sanitize_filename(fname)}\n") fname = "\0_a*b:ce%f/(g)h+i_0.txt" print(f"{fname} -> {sanitize_filename(fname)}\n") :Output: .. code-block:: none fi:l*e/p"a?t>h|.t filepath.txt _a*b:ce%f/(g)h+i_0.txt -> _abcde%f(g)h+i_0.txt The default target ``platform`` is ``universal``. i.e. the sanitized file name is valid for any platform. pathvalidate-3.2.0/docs/pages/examples/sanitize_filepath_code.txt000066400000000000000000000006671450146604500253020ustar00rootroot00000000000000:Sample Code: .. code-block:: python from pathvalidate import sanitize_filepath fpath = "fi:l*e/p\"a?t>h|.t {sanitize_filepath(fpath)}\n") fpath = "\0_a*b:ce%f/(g)h+i_0.txt" print(f"{fpath} -> {sanitize_filepath(fpath)}\n") :Output: .. code-block:: none fi:l*e/p"a?t>h|.t file/path.txt _a*b:ce%f/(g)h+i_0.txt -> _abcde%f/(g)h+i_0.txt pathvalidate-3.2.0/docs/pages/examples/sanitize_replace_symbol_code.txt000066400000000000000000000004221450146604500264730ustar00rootroot00000000000000:Sample Code: .. code-block:: python from pathvalidate import replace_symbol name = "\0_a*b:ce%f/(g)h+i_0.txt" print(f"{name} -> {replace_symbol(name)}") :Output: .. code-block:: none _a*b:ce%f/(g)h+i_0.txt -> abcdefghi0txt pathvalidate-3.2.0/docs/pages/examples/sanitize_var_name_code.txt000066400000000000000000000003201450146604500252600ustar00rootroot00000000000000:Sample Code: .. code-block:: python import pathvalidate as pv print(pv.sanitize_python_var_name("_a*b:ce%f/(g)h+i_0.txt")) :Output: .. code-block:: none abcdefghi_0txt pathvalidate-3.2.0/docs/pages/examples/validate.rst000066400000000000000000000007611450146604500223630ustar00rootroot00000000000000.. _example-validate-filename: Validate a filename ---------------------------- The :py:func:`.validate_filename()` function raise ``ValueError`` if the name includes invalid character(s) for a filename. .. include:: validate_filename_code.txt .. _example-validate-file-path: Validate a file path ---------------------------- The :py:func:`.validate_filepath()` function raise ``ValueError`` if the name includes invalid character(s) for a file path. .. include:: validate_filepath_code.txt pathvalidate-3.2.0/docs/pages/examples/validate_filename_code.txt000066400000000000000000000012641450146604500252230ustar00rootroot00000000000000:Sample Code: .. code-block:: python import sys from pathvalidate import ValidationError, validate_filename try: validate_filename("fi:l*e/p\"a?t>h|.th|.t', '|', '<'), value='fi:l*e/p"a?t>h|.te%f/(g)h+i_0.txt") except ValueError: print("invalid variable name!") :Output: .. code-block:: none invalid variable name! pathvalidate-3.2.0/docs/pages/genindex.rst000066400000000000000000000000701450146604500205460ustar00rootroot00000000000000Indices and tables ================== * :ref:`genindex`pathvalidate-3.2.0/docs/pages/introduction/000077500000000000000000000000001450146604500207375ustar00rootroot00000000000000pathvalidate-3.2.0/docs/pages/introduction/badges.txt000066400000000000000000000023541450146604500227310ustar00rootroot00000000000000.. image:: https://badge.fury.io/py/pathvalidate.svg :target: https://badge.fury.io/py/pathvalidate :alt: PyPI package version .. image:: https://anaconda.org/thombashi/pathvalidate/badges/version.svg :target: https://anaconda.org/thombashi/pathvalidate :alt: conda package version .. image:: https://img.shields.io/pypi/pyversions/pathvalidate.svg :target: https://pypi.org/project/pathvalidate :alt: Supported Python versions .. image:: https://img.shields.io/pypi/implementation/pathvalidate.svg :target: https://pypi.org/project/pathvalidate :alt: Supported Python implementations .. image:: https://github.com/thombashi/pathvalidate/workflows/Tests/badge.svg :target: https://github.com/thombashi/pathvalidate/actions?query=workflow%3ATests :alt: Linux/macOS/Windows CI status .. image:: https://coveralls.io/repos/github/thombashi/pathvalidate/badge.svg?branch=master :target: https://coveralls.io/github/thombashi/pathvalidate?branch=master :alt: Test coverage: coveralls .. image:: https://github.com/thombashi/pathvalidate/actions/workflows/github-code-scanning/codeql/badge.svg :target: https://github.com/thombashi/pathvalidate/actions/workflows/github-code-scanning/codeql :alt: CodeQL pathvalidate-3.2.0/docs/pages/introduction/feature.txt000066400000000000000000000007521450146604500231370ustar00rootroot00000000000000Features --------- - Sanitize/Validate a string as a: - file name - file path - Sanitize will do: - Remove invalid characters for a target platform - Replace reserved names for a target platform - Normalize - Remove unprintable characters - Argument validator/sanitizer for ``argparse`` and ``click`` - Multi platform support: - ``Linux`` - ``Windows`` - ``macOS`` - ``POSIX`` - ``universal`` (platform independent) - Multibyte character support pathvalidate-3.2.0/docs/pages/introduction/index.rst000066400000000000000000000006271450146604500226050ustar00rootroot00000000000000pathvalidate ============= .. include:: badges.txt Summary ------- .. include:: summary.txt .. raw:: html

.. include:: feature.txt .. include:: installation.rst pathvalidate-3.2.0/docs/pages/introduction/installation.rst000066400000000000000000000006641450146604500242000ustar00rootroot00000000000000Installation ============ Installation: pip ------------------------------ :: pip install pathvalidate Installation: conda ------------------------------ :: conda install -c thombashi pathvalidate Installation: apt ------------------------------ :: sudo add-apt-repository ppa:thombashi/ppa sudo apt update sudo apt install python3-pathvalidate Dependencies ============ Python 3.7+ no external dependencies. pathvalidate-3.2.0/docs/pages/introduction/summary.txt000066400000000000000000000001411450146604500231710ustar00rootroot00000000000000pathvalidate is a Python library to sanitize/validate a string such as filenames/file-paths/etc. pathvalidate-3.2.0/docs/pages/links.rst000066400000000000000000000005501450146604500200700ustar00rootroot00000000000000Changelog ========== https://github.com/thombashi/pathvalidate/releases .. include:: sponsors.rst .. include:: genindex.rst Links ===== - `GitHub repository `__ - `Issue tracker `__ - `pip: A tool for installing Python packages `__ pathvalidate-3.2.0/docs/pages/reference/000077500000000000000000000000001450146604500201545ustar00rootroot00000000000000pathvalidate-3.2.0/docs/pages/reference/error.rst000066400000000000000000000035321450146604500220420ustar00rootroot00000000000000Errors --------------- .. autoclass:: pathvalidate.error.ErrorReason :members: :undoc-members: :show-inheritance: .. table:: Liar od Errors +--------+------------------------+------------------------------------------------------+ | Code | Name | Description | +========+========================+======================================================+ | PV1001 | NULL_NAME | the value must not be an empty | +--------+------------------------+------------------------------------------------------+ | PV1002 | RESERVED_NAME | found a reserved name by a platform | +--------+------------------------+------------------------------------------------------+ | PV1100 | INVALID_CHARACTER | invalid characters found | +--------+------------------------+------------------------------------------------------+ | PV1101 | INVALID_LENGTH | found an invalid string length | +--------+------------------------+------------------------------------------------------+ | PV1200 | FOUND_ABS_PATH | found an absolute path where must be a relative path | +--------+------------------------+------------------------------------------------------+ | PV1201 | MALFORMED_ABS_PATH | found a malformed absolute path | +--------+------------------------+------------------------------------------------------+ | PV2000 | INVALID_AFTER_SANITIZE | found invalid value after sanitizing | +--------+------------------------+------------------------------------------------------+ .. autoexception:: pathvalidate.error.ValidationError :members: :undoc-members: :show-inheritance: pathvalidate-3.2.0/docs/pages/reference/function.rst000066400000000000000000000013561450146604500225400ustar00rootroot00000000000000Functions --------------- File name validation/sanitization ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. autofunction:: pathvalidate.validate_filename .. autofunction:: pathvalidate.sanitize_filename Check a file name ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. autofunction:: pathvalidate.is_valid_filename File path validation/sanitization ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. autofunction:: pathvalidate.validate_filepath .. autofunction:: pathvalidate.sanitize_filepath Check a file path ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. autofunction:: pathvalidate.is_valid_filepath Symbol validation/sanitization ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. autofunction:: pathvalidate.validate_symbol .. autofunction:: pathvalidate.replace_symbol pathvalidate-3.2.0/docs/pages/reference/handler.rst000066400000000000000000000006071450146604500223260ustar00rootroot00000000000000Handlers --------------- Reserved name handlers ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. autoclass:: pathvalidate.handler.ReservedNameHandler :members: Null value handlers ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. autoclass:: pathvalidate.handler.NullValueHandler :members: Other handlers ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. autofunction:: pathvalidate.handler.raise_error pathvalidate-3.2.0/docs/pages/reference/index.rst000066400000000000000000000001371450146604500220160ustar00rootroot00000000000000Reference ========= .. toctree:: :maxdepth: 3 error function types handler pathvalidate-3.2.0/docs/pages/reference/platform.txt000066400000000000000000000004641450146604500225450ustar00rootroot00000000000000Valid specifiers are as follows (case-insensitive): - ``"Linux"`` - ``"Windows"`` - ``"macOS"`` - ``"auto"``: automatically detect the execution platform - ``"POSIX"`` - ``"universal"``/|None|: platform independent. note that absolute paths cannot specify this. Defaults to |None|. pathvalidate-3.2.0/docs/pages/reference/types.rst000066400000000000000000000001651450146604500220540ustar00rootroot00000000000000Types --------------- .. autoclass:: pathvalidate.Platform :members: :undoc-members: :show-inheritance: pathvalidate-3.2.0/docs/pages/sponsors.rst000066400000000000000000000013711450146604500206400ustar00rootroot00000000000000Sponsors ==================================== .. image:: https://avatars.githubusercontent.com/u/44389260?s=48&u=6da7176e51ae2654bcfd22564772ef8a3bb22318&v=4 :target: https://github.com/chasbecker :alt: Charles Becker (chasbecker) .. image:: https://avatars.githubusercontent.com/u/9919?s=48&v=4 :target: https://github.com/github :alt: onetime: GitHub (github) .. image:: https://avatars.githubusercontent.com/u/46711571?s=48&u=57687c0e02d5d6e8eeaf9177f7b7af4c9f275eb5&v=4 :target: https://github.com/Arturi0 :alt: onetime: Arturi0 .. image:: https://avatars.githubusercontent.com/u/3658062?s=48&v=4 :target: https://github.com/b4tman :alt: onetime: Dmitry Belyaev (b4tman) `Become a sponsor `__ pathvalidate-3.2.0/examples/000077500000000000000000000000001450146604500160055ustar00rootroot00000000000000pathvalidate-3.2.0/examples/README.rst000066400000000000000000000001401450146604500174670ustar00rootroot00000000000000https://nbviewer.jupyter.org/github/thombashi/pathvalidate/tree/master/ipynb/pathvalidate.ipynb pathvalidate-3.2.0/examples/argparse_sanitize.py000077500000000000000000000007041450146604500220750ustar00rootroot00000000000000#!/usr/bin/env python3 from argparse import ArgumentParser from pathvalidate.argparse import sanitize_filename_arg, sanitize_filepath_arg parser = ArgumentParser() parser.add_argument("--filename", type=sanitize_filename_arg) parser.add_argument("--filepath", type=sanitize_filepath_arg) options = parser.parse_args() if options.filename: print(f"filename: {options.filename}") if options.filepath: print(f"filepath: {options.filepath}") pathvalidate-3.2.0/examples/argparse_validate.py000077500000000000000000000007041450146604500220400ustar00rootroot00000000000000#!/usr/bin/env python3 from argparse import ArgumentParser from pathvalidate.argparse import validate_filename_arg, validate_filepath_arg parser = ArgumentParser() parser.add_argument("--filename", type=validate_filename_arg) parser.add_argument("--filepath", type=validate_filepath_arg) options = parser.parse_args() if options.filename: print(f"filename: {options.filename}") if options.filepath: print(f"filepath: {options.filepath}") pathvalidate-3.2.0/examples/click_sanitize.py000077500000000000000000000007151450146604500213600ustar00rootroot00000000000000#!/usr/bin/env python3 import click from pathvalidate.click import sanitize_filename_arg, sanitize_filepath_arg @click.command() @click.option("--filename", callback=sanitize_filename_arg) @click.option("--filepath", callback=sanitize_filepath_arg) def cli(filename: str, filepath: str) -> None: if filename: click.echo(f"filename: {filename}") if filepath: click.echo(f"filepath: {filepath}") if __name__ == "__main__": cli() pathvalidate-3.2.0/examples/click_validate.py000077500000000000000000000007151450146604500213230ustar00rootroot00000000000000#!/usr/bin/env python3 import click from pathvalidate.click import validate_filename_arg, validate_filepath_arg @click.command() @click.option("--filename", callback=validate_filename_arg) @click.option("--filepath", callback=validate_filepath_arg) def cli(filename: str, filepath: str) -> None: if filename: click.echo(f"filename: {filename}") if filepath: click.echo(f"filepath: {filepath}") if __name__ == "__main__": cli() pathvalidate-3.2.0/examples/pathvalidate_examples.ipynb000066400000000000000000000147231450146604500234230ustar00rootroot00000000000000{ "cells": [ { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "name = \"\\0_a*b:ce%f/(g)h+i_0.txt\"" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "[PV1100] invalid characters found: invalids=('/'), value='fi:l*e/p\"a?t>h|.th|.t', '|', '<'), value='fi:l*e/p\"a?t>h|.th|.th|.t filepath.txt\n", "\n", "\u0000_a*b:ce%f/(g)h+i_0.txt -> _abcde%f(g)h+i_0.txt\n", "\n" ] } ], "source": [ "from pathvalidate import sanitize_filename\n", "\n", "fname = \"fi:l*e/p\\\"a?t>h|.t {sanitize_filename(fname)}\\n\")\n", "\n", "fname = \"\\0_a*b:ce%f/(g)h+i_0.txt\"\n", "print(f\"{fname} -> {sanitize_filename(fname)}\\n\")" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "fi:l*e/p\"a?t>h|.t file/path.txt\n", "\n", "\u0000_a*b:ce%f/(g)h+i_0.txt -> _abcde%f/(g)h+i_0.txt\n", "\n" ] } ], "source": [ "from pathvalidate import sanitize_filepath\n", "\n", "fpath = \"fi:l*e/p\\\"a?t>h|.t {sanitize_filepath(fpath)}\\n\")\n", "\n", "fpath = \"\\0_a*b:ce%f/(g)h+i_0.txt\"\n", "print(f\"{fpath} -> {sanitize_filepath(fpath)}\\n\")" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u0000_a*b:ce%f/(g)h+i_0.txt -> abcdefghi0txt\n" ] } ], "source": [ "from pathvalidate import replace_symbol\n", "\n", "name = \"\\0_a*b:ce%f/(g)h+i_0.txt\"\n", "print(f\"{name} -> {replace_symbol(name)}\")" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "is_valid_filename('fi:l*e/p\"a?t>h|.th|.th|.th|.t str:\n", " if e.reusable_name:\n", " return e.reserved_name\n", "\n", " return f\"{e.reserved_name}_\"\n", "\n", "sanitize_filename(\".\", reserved_name_handler=add_trailing_underscore, additional_reserved_names=[\".\"])\n" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3.8.12 64-bit ('3.8.12')", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.4" }, "vscode": { "interpreter": { "hash": "c07eda5d3696c8b68c933d32ea433f84251ada7607722ea42bf003255d06d542" } } }, "nbformat": 4, "nbformat_minor": 1 } pathvalidate-3.2.0/pathvalidate/000077500000000000000000000000001450146604500166355ustar00rootroot00000000000000pathvalidate-3.2.0/pathvalidate/__init__.py000066400000000000000000000036061450146604500207530ustar00rootroot00000000000000""" .. codeauthor:: Tsuyoshi Hombashi """ from .__version__ import __author__, __copyright__, __email__, __license__, __version__ from ._base import AbstractSanitizer, AbstractValidator from ._common import ( ascii_symbols, normalize_platform, replace_ansi_escape, replace_unprintable_char, unprintable_ascii_chars, validate_pathtype, validate_unprintable_char, ) from ._const import Platform from ._filename import ( FileNameSanitizer, FileNameValidator, is_valid_filename, sanitize_filename, validate_filename, ) from ._filepath import ( FilePathSanitizer, FilePathValidator, is_valid_filepath, sanitize_filepath, validate_filepath, ) from ._ltsv import sanitize_ltsv_label, validate_ltsv_label from ._symbol import replace_symbol, validate_symbol from .error import ( ErrorReason, InvalidCharError, InvalidReservedNameError, NullNameError, ReservedNameError, ValidationError, ValidReservedNameError, ) __all__ = ( "__author__", "__copyright__", "__email__", "__license__", "__version__", "AbstractSanitizer", "AbstractValidator", "Platform", "ascii_symbols", "normalize_platform", "replace_ansi_escape", "replace_unprintable_char", "unprintable_ascii_chars", "validate_pathtype", "validate_unprintable_char", "FileNameSanitizer", "FileNameValidator", "is_valid_filename", "sanitize_filename", "validate_filename", "FilePathSanitizer", "FilePathValidator", "is_valid_filepath", "sanitize_filepath", "validate_filepath", "sanitize_ltsv_label", "validate_ltsv_label", "replace_symbol", "validate_symbol", "ErrorReason", "InvalidCharError", "InvalidReservedNameError", "NullNameError", "ReservedNameError", "ValidationError", "ValidReservedNameError", ) pathvalidate-3.2.0/pathvalidate/__version__.py000066400000000000000000000003111450146604500214630ustar00rootroot00000000000000__author__ = "Tsuyoshi Hombashi" __copyright__ = f"Copyright 2016, {__author__}" __license__ = "MIT License" __version__ = "3.2.0" __maintainer__ = __author__ __email__ = "tsuyoshi.hombashi@gmail.com" pathvalidate-3.2.0/pathvalidate/_base.py000066400000000000000000000164231450146604500202660ustar00rootroot00000000000000""" .. codeauthor:: Tsuyoshi Hombashi """ import abc import os import sys from typing import ClassVar, Optional, Sequence, Tuple from ._common import normalize_platform, unprintable_ascii_chars from ._const import DEFAULT_MIN_LEN, Platform from ._types import PathType, PlatformType from .error import ReservedNameError, ValidationError from .handler import NullValueHandler, ReservedNameHandler, ValidationErrorHandler class BaseFile: _INVALID_PATH_CHARS: ClassVar[str] = "".join(unprintable_ascii_chars) _INVALID_FILENAME_CHARS: ClassVar[str] = _INVALID_PATH_CHARS + "/" _INVALID_WIN_PATH_CHARS: ClassVar[str] = _INVALID_PATH_CHARS + ':*?"<>|\t\n\r\x0b\x0c' _INVALID_WIN_FILENAME_CHARS: ClassVar[str] = ( _INVALID_FILENAME_CHARS + _INVALID_WIN_PATH_CHARS + "\\" ) @property def platform(self) -> Platform: return self.__platform @property def reserved_keywords(self) -> Tuple[str, ...]: return self._additional_reserved_names @property def max_len(self) -> int: return self._max_len def __init__( self, max_len: int, fs_encoding: Optional[str], additional_reserved_names: Optional[Sequence[str]] = None, platform_max_len: Optional[int] = None, platform: Optional[PlatformType] = None, ) -> None: if additional_reserved_names is None: additional_reserved_names = tuple() self._additional_reserved_names = tuple(n.upper() for n in additional_reserved_names) self.__platform = normalize_platform(platform) if platform_max_len is None: platform_max_len = self._get_default_max_path_len() if max_len <= 0: self._max_len = platform_max_len else: self._max_len = max_len self._max_len = min(self._max_len, platform_max_len) if fs_encoding: self._fs_encoding = fs_encoding else: self._fs_encoding = sys.getfilesystemencoding() def _is_posix(self) -> bool: return self.platform == Platform.POSIX def _is_universal(self) -> bool: return self.platform == Platform.UNIVERSAL def _is_linux(self, include_universal: bool = False) -> bool: if include_universal: return self.platform in (Platform.UNIVERSAL, Platform.LINUX) return self.platform == Platform.LINUX def _is_windows(self, include_universal: bool = False) -> bool: if include_universal: return self.platform in (Platform.UNIVERSAL, Platform.WINDOWS) return self.platform == Platform.WINDOWS def _is_macos(self, include_universal: bool = False) -> bool: if include_universal: return self.platform in (Platform.UNIVERSAL, Platform.MACOS) return self.platform == Platform.MACOS def _get_default_max_path_len(self) -> int: if self._is_linux(): return 4096 if self._is_windows(): return 260 if self._is_posix() or self._is_macos(): return 1024 return 260 # universal class AbstractValidator(BaseFile, metaclass=abc.ABCMeta): def __init__( self, max_len: int, fs_encoding: Optional[str], check_reserved: bool, additional_reserved_names: Optional[Sequence[str]] = None, platform_max_len: Optional[int] = None, platform: Optional[PlatformType] = None, ) -> None: self._check_reserved = check_reserved super().__init__( max_len, fs_encoding, additional_reserved_names=additional_reserved_names, platform_max_len=platform_max_len, platform=platform, ) @abc.abstractproperty def min_len(self) -> int: # pragma: no cover pass @abc.abstractmethod def validate(self, value: PathType) -> None: # pragma: no cover pass def is_valid(self, value: PathType) -> bool: try: self.validate(value) except (TypeError, ValidationError): return False return True def _is_reserved_keyword(self, value: str) -> bool: return value in self.reserved_keywords class AbstractSanitizer(BaseFile, metaclass=abc.ABCMeta): def __init__( self, validator: AbstractValidator, max_len: int, fs_encoding: Optional[str], validate_after_sanitize: bool, null_value_handler: Optional[ValidationErrorHandler] = None, reserved_name_handler: Optional[ValidationErrorHandler] = None, additional_reserved_names: Optional[Sequence[str]] = None, platform_max_len: Optional[int] = None, platform: Optional[PlatformType] = None, ) -> None: super().__init__( max_len=max_len, fs_encoding=fs_encoding, additional_reserved_names=additional_reserved_names, platform_max_len=platform_max_len, platform=platform, ) if null_value_handler is None: null_value_handler = NullValueHandler.return_null_string self._null_value_handler = null_value_handler if reserved_name_handler is None: reserved_name_handler = ReservedNameHandler.add_trailing_underscore self._reserved_name_handler = reserved_name_handler self._validate_after_sanitize = validate_after_sanitize self._validator = validator @abc.abstractmethod def sanitize(self, value: PathType, replacement_text: str = "") -> PathType: # pragma: no cover pass class BaseValidator(AbstractValidator): @property def min_len(self) -> int: return self._min_len def __init__( self, min_len: int, max_len: int, fs_encoding: Optional[str], check_reserved: bool, additional_reserved_names: Optional[Sequence[str]] = None, platform_max_len: Optional[int] = None, platform: Optional[PlatformType] = None, ) -> None: if min_len <= 0: min_len = DEFAULT_MIN_LEN self._min_len = max(min_len, 1) super().__init__( max_len=max_len, fs_encoding=fs_encoding, check_reserved=check_reserved, additional_reserved_names=additional_reserved_names, platform_max_len=platform_max_len, platform=platform, ) self._validate_max_len() def _validate_reserved_keywords(self, name: str) -> None: if not self._check_reserved: 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, ) def _validate_max_len(self) -> None: if self.max_len < 1: raise ValueError("max_len must be greater or equal to one") if self.min_len > self.max_len: raise ValueError("min_len must be lower than max_len") @staticmethod def __extract_root_name(path: str) -> str: return os.path.splitext(os.path.basename(path))[0] pathvalidate-3.2.0/pathvalidate/_common.py000066400000000000000000000064431450146604500206450ustar00rootroot00000000000000""" .. codeauthor:: Tsuyoshi Hombashi """ import platform import re import string from pathlib import PurePath from typing import Any, List, Optional from ._const import Platform from ._types import PathType, PlatformType _re_whitespaces = re.compile(r"^[\s]+$") def validate_pathtype( text: PathType, allow_whitespaces: bool = False, error_msg: Optional[str] = None ) -> None: from .error import ErrorReason, ValidationError if _is_not_null_string(text) or isinstance(text, PurePath): return if allow_whitespaces and _re_whitespaces.search(str(text)): return if is_null_string(text): raise ValidationError(reason=ErrorReason.NULL_NAME) raise TypeError(f"text must be a string: actual={type(text)}") def to_str(name: PathType) -> str: if isinstance(name, PurePath): return str(name) return name def is_null_string(value: Any) -> bool: if value is None: return True try: return len(value.strip()) == 0 except AttributeError: return False def _is_not_null_string(value: Any) -> bool: try: return len(value.strip()) > 0 except AttributeError: return False def _get_unprintable_ascii_chars() -> List[str]: return [chr(c) for c in range(128) if chr(c) not in string.printable] unprintable_ascii_chars = tuple(_get_unprintable_ascii_chars()) def _get_ascii_symbols() -> List[str]: symbol_list: List[str] = [] for i in range(128): c = chr(i) if c in unprintable_ascii_chars or c in string.digits + string.ascii_letters: continue symbol_list.append(c) return symbol_list ascii_symbols = tuple(_get_ascii_symbols()) __RE_UNPRINTABLE_CHARS = re.compile( "[{}]".format(re.escape("".join(unprintable_ascii_chars))), re.UNICODE ) __RE_ANSI_ESCAPE = re.compile( r"(?:\x1B[@-Z\\-_]|[\x80-\x9A\x9C-\x9F]|(?:\x1B\[|\x9B)[0-?]*[ -/]*[@-~])" ) def validate_unprintable_char(text: str) -> None: from .error import InvalidCharError match_list = __RE_UNPRINTABLE_CHARS.findall(to_str(text)) if match_list: raise InvalidCharError(f"unprintable character found: {match_list}") def replace_unprintable_char(text: str, replacement_text: str = "") -> str: try: return __RE_UNPRINTABLE_CHARS.sub(replacement_text, text) except (TypeError, AttributeError): raise TypeError("text must be a string") def replace_ansi_escape(text: str, replacement_text: str = "") -> str: try: return __RE_ANSI_ESCAPE.sub(replacement_text, text) except (TypeError, AttributeError): raise TypeError("text must be a string") def normalize_platform(name: Optional[PlatformType]) -> Platform: if isinstance(name, Platform): return name if not name: return Platform.UNIVERSAL name = name.strip().casefold() if name == "posix": return Platform.POSIX if name == "auto": name = platform.system().casefold() if name in ["linux"]: return Platform.LINUX if name and name.startswith("win"): return Platform.WINDOWS if name in ["mac", "macos", "darwin"]: return Platform.MACOS return Platform.UNIVERSAL def findall_to_str(match: List[Any]) -> str: return ", ".join([repr(text) for text in match]) pathvalidate-3.2.0/pathvalidate/_const.py000066400000000000000000000012561450146604500205000ustar00rootroot00000000000000import enum DEFAULT_MIN_LEN = 1 INVALID_CHAR_ERR_MSG_TMPL = "invalids=({invalid}), value={value}" _NTFS_RESERVED_FILE_NAMES = ( "$Mft", "$MftMirr", "$LogFile", "$Volume", "$AttrDef", "$Bitmap", "$Boot", "$BadClus", "$Secure", "$Upcase", "$Extend", "$Quota", "$ObjId", "$Reparse", ) # Only in root directory @enum.unique class Platform(enum.Enum): """ Platform specifier enumeration. """ #: POSIX compatible platform. POSIX = "POSIX" #: platform independent. note that absolute paths cannot specify this. UNIVERSAL = "universal" LINUX = "Linux" WINDOWS = "Windows" MACOS = "macOS" pathvalidate-3.2.0/pathvalidate/_filename.py000066400000000000000000000417141450146604500211350ustar00rootroot00000000000000""" .. codeauthor:: Tsuyoshi Hombashi """ import itertools import ntpath import posixpath import re import warnings from pathlib import Path, PurePath from typing import Optional, Pattern, Sequence, Tuple from ._base import AbstractSanitizer, AbstractValidator, BaseFile, BaseValidator from ._common import findall_to_str, to_str, validate_pathtype from ._const import DEFAULT_MIN_LEN, INVALID_CHAR_ERR_MSG_TMPL, Platform from ._types import PathType, PlatformType from .error import ErrorAttrKey, ErrorReason, InvalidCharError, ValidationError from .handler import ReservedNameHandler, ValidationErrorHandler _DEFAULT_MAX_FILENAME_LEN = 255 _RE_INVALID_FILENAME = re.compile(f"[{re.escape(BaseFile._INVALID_FILENAME_CHARS):s}]", re.UNICODE) _RE_INVALID_WIN_FILENAME = re.compile( f"[{re.escape(BaseFile._INVALID_WIN_FILENAME_CHARS):s}]", re.UNICODE ) class FileNameSanitizer(AbstractSanitizer): def __init__( self, max_len: int = _DEFAULT_MAX_FILENAME_LEN, fs_encoding: Optional[str] = None, platform: Optional[PlatformType] = None, null_value_handler: Optional[ValidationErrorHandler] = None, reserved_name_handler: Optional[ValidationErrorHandler] = None, additional_reserved_names: Optional[Sequence[str]] = None, validate_after_sanitize: bool = False, validator: Optional[AbstractValidator] = None, ) -> None: if validator: fname_validator = validator else: fname_validator = FileNameValidator( min_len=DEFAULT_MIN_LEN, max_len=max_len, fs_encoding=fs_encoding, check_reserved=True, additional_reserved_names=additional_reserved_names, platform=platform, ) super().__init__( max_len=max_len, fs_encoding=fs_encoding, null_value_handler=null_value_handler, reserved_name_handler=reserved_name_handler, additional_reserved_names=additional_reserved_names, platform_max_len=_DEFAULT_MAX_FILENAME_LEN, platform=platform, validate_after_sanitize=validate_after_sanitize, validator=fname_validator, ) self._sanitize_regexp = self._get_sanitize_regexp() def sanitize(self, value: PathType, replacement_text: str = "") -> PathType: try: validate_pathtype(value, allow_whitespaces=not self._is_windows(include_universal=True)) except ValidationError as e: if e.reason == ErrorReason.NULL_NAME: if isinstance(value, PurePath): raise return self._null_value_handler(e) raise sanitized_filename = self._sanitize_regexp.sub(replacement_text, str(value)) sanitized_filename = sanitized_filename[: self.max_len] try: self._validator.validate(sanitized_filename) except ValidationError as e: if e.reason == ErrorReason.RESERVED_NAME: replacement_word = self._reserved_name_handler(e) if e.reserved_name != replacement_word: sanitized_filename = re.sub( re.escape(e.reserved_name), replacement_word, sanitized_filename ) elif e.reason == ErrorReason.INVALID_CHARACTER and self._is_windows( include_universal=True ): # Do not start a file or directory name with a space sanitized_filename = sanitized_filename.lstrip(" ") # Do not end a file or directory name with a space or a period sanitized_filename = sanitized_filename.rstrip(" ") if sanitized_filename not in (".", ".."): sanitized_filename = sanitized_filename.rstrip(" .") elif e.reason == ErrorReason.NULL_NAME: sanitized_filename = self._null_value_handler(e) if self._validate_after_sanitize: try: self._validator.validate(sanitized_filename) except ValidationError as e: raise ValidationError( description=str(e), reason=ErrorReason.INVALID_AFTER_SANITIZE, platform=self.platform, ) if isinstance(value, PurePath): return Path(sanitized_filename) return sanitized_filename def _get_sanitize_regexp(self) -> Pattern[str]: if self._is_windows(include_universal=True): return _RE_INVALID_WIN_FILENAME return _RE_INVALID_FILENAME class FileNameValidator(BaseValidator): _WINDOWS_RESERVED_FILE_NAMES = ("CON", "PRN", "AUX", "CLOCK$", "NUL") + tuple( f"{name:s}{num:d}" for name, num in itertools.product(("COM", "LPT"), range(1, 10)) ) _MACOS_RESERVED_FILE_NAMES = (":",) @property def reserved_keywords(self) -> Tuple[str, ...]: common_keywords = super().reserved_keywords if self._is_universal(): word_set = set( common_keywords + self._WINDOWS_RESERVED_FILE_NAMES + self._MACOS_RESERVED_FILE_NAMES ) elif self._is_windows(): word_set = set(common_keywords + self._WINDOWS_RESERVED_FILE_NAMES) elif self._is_posix() or self._is_macos(): word_set = set(common_keywords + self._MACOS_RESERVED_FILE_NAMES) else: word_set = set(common_keywords) return tuple(sorted(word_set)) def __init__( self, min_len: int = DEFAULT_MIN_LEN, max_len: int = _DEFAULT_MAX_FILENAME_LEN, fs_encoding: Optional[str] = None, platform: Optional[PlatformType] = None, check_reserved: bool = True, additional_reserved_names: Optional[Sequence[str]] = None, ) -> None: super().__init__( min_len=min_len, max_len=max_len, fs_encoding=fs_encoding, check_reserved=check_reserved, additional_reserved_names=additional_reserved_names, platform_max_len=_DEFAULT_MAX_FILENAME_LEN, platform=platform, ) def validate(self, value: PathType) -> None: validate_pathtype(value, allow_whitespaces=not self._is_windows(include_universal=True)) unicode_filename = to_str(value) byte_ct = len(unicode_filename.encode(self._fs_encoding)) self.validate_abspath(unicode_filename) err_kwargs = { ErrorAttrKey.REASON: ErrorReason.INVALID_LENGTH, ErrorAttrKey.PLATFORM: self.platform, ErrorAttrKey.FS_ENCODING: self._fs_encoding, ErrorAttrKey.BYTE_COUNT: byte_ct, } if byte_ct > self.max_len: raise ValidationError( [ f"filename is too long: expected<={self.max_len:d} bytes, actual={byte_ct:d} bytes" ], **err_kwargs, ) if byte_ct < self.min_len: raise ValidationError( [ f"filename is too short: expected>={self.min_len:d} bytes, actual={byte_ct:d} bytes" ], **err_kwargs, ) self._validate_reserved_keywords(unicode_filename) self.__validate_universal_filename(unicode_filename) if self._is_windows(include_universal=True): self.__validate_win_filename(unicode_filename) def validate_abspath(self, value: str) -> None: err = ValidationError( description=f"found an absolute path ({value}), expected a filename", platform=self.platform, reason=ErrorReason.FOUND_ABS_PATH, ) if self._is_windows(include_universal=True): if ntpath.isabs(value): raise err if posixpath.isabs(value): raise err def __validate_universal_filename(self, unicode_filename: str) -> None: match = _RE_INVALID_FILENAME.findall(unicode_filename) if match: raise InvalidCharError( INVALID_CHAR_ERR_MSG_TMPL.format( invalid=findall_to_str(match), value=repr(unicode_filename) ), platform=Platform.UNIVERSAL, ) def __validate_win_filename(self, unicode_filename: str) -> None: match = _RE_INVALID_WIN_FILENAME.findall(unicode_filename) if match: raise InvalidCharError( INVALID_CHAR_ERR_MSG_TMPL.format( invalid=findall_to_str(match), value=repr(unicode_filename) ), platform=Platform.WINDOWS, ) if unicode_filename in (".", ".."): return KB2829981_err_tmpl = "{}. Refer: https://learn.microsoft.com/en-us/troubleshoot/windows-client/shell-experience/file-folder-name-whitespace-characters" # noqa: E501 if unicode_filename[-1] in (" ", "."): raise InvalidCharError( INVALID_CHAR_ERR_MSG_TMPL.format( invalid=re.escape(unicode_filename[-1]), value=repr(unicode_filename) ), platform=Platform.WINDOWS, description=KB2829981_err_tmpl.format( "Do not end a file or directory name with a space or a period" ), ) if unicode_filename[0] in (" "): raise InvalidCharError( INVALID_CHAR_ERR_MSG_TMPL.format( invalid=re.escape(unicode_filename[0]), value=repr(unicode_filename) ), platform=Platform.WINDOWS, description=KB2829981_err_tmpl.format( "Do not start a file or directory name with a space" ), ) def validate_filename( filename: PathType, platform: Optional[PlatformType] = None, min_len: int = DEFAULT_MIN_LEN, max_len: int = _DEFAULT_MAX_FILENAME_LEN, fs_encoding: Optional[str] = None, check_reserved: bool = True, additional_reserved_names: Optional[Sequence[str]] = None, ) -> None: """Verifying whether the ``filename`` is a valid file name or not. Args: filename: Filename to validate. platform: Target platform name of the filename. .. include:: platform.txt min_len: Minimum byte length of the ``filename``. The value must be greater or equal to one. Defaults to ``1``. max_len: Maximum byte length of the ``filename``. The value must be lower than: - ``Linux``: 4096 - ``macOS``: 1024 - ``Windows``: 260 - ``universal``: 260 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. check_reserved: If |True|, check reserved names of the ``platform``. additional_reserved_names: Additional reserved names to check. Case insensitive. Raises: ValidationError (ErrorReason.INVALID_LENGTH): If the ``filename`` is longer than ``max_len`` characters. ValidationError (ErrorReason.INVALID_CHARACTER): If the ``filename`` includes invalid character(s) for a filename: |invalid_filename_chars|. The following characters are also invalid for Windows platforms: |invalid_win_filename_chars|. ValidationError (ErrorReason.RESERVED_NAME): If the ``filename`` equals reserved name by OS. Windows reserved name is as follows: ``"CON"``, ``"PRN"``, ``"AUX"``, ``"NUL"``, ``"COM[1-9]"``, ``"LPT[1-9]"``. Example: :ref:`example-validate-filename` See Also: `Naming Files, Paths, and Namespaces - Win32 apps | Microsoft Docs `__ """ FileNameValidator( platform=platform, min_len=min_len, max_len=max_len, fs_encoding=fs_encoding, check_reserved=check_reserved, additional_reserved_names=additional_reserved_names, ).validate(filename) def is_valid_filename( filename: PathType, platform: Optional[PlatformType] = None, min_len: int = DEFAULT_MIN_LEN, max_len: Optional[int] = None, fs_encoding: Optional[str] = None, check_reserved: bool = True, additional_reserved_names: Optional[Sequence[str]] = None, ) -> bool: """Check whether the ``filename`` is a valid name or not. Args: filename: A filename to be checked. platform: Target platform name of the filename. Example: :ref:`example-is-valid-filename` See Also: :py:func:`.validate_filename()` """ return FileNameValidator( platform=platform, min_len=min_len, max_len=-1 if max_len is None else max_len, fs_encoding=fs_encoding, check_reserved=check_reserved, additional_reserved_names=additional_reserved_names, ).is_valid(filename) def sanitize_filename( filename: PathType, replacement_text: str = "", platform: Optional[PlatformType] = None, max_len: Optional[int] = _DEFAULT_MAX_FILENAME_LEN, fs_encoding: Optional[str] = None, check_reserved: Optional[bool] = None, null_value_handler: Optional[ValidationErrorHandler] = None, reserved_name_handler: Optional[ValidationErrorHandler] = None, additional_reserved_names: Optional[Sequence[str]] = None, validate_after_sanitize: bool = False, ) -> PathType: """Make a valid filename from a string. To make a valid filename, the function does the following: - Replace invalid characters as file names included in the ``filename`` with the ``replacement_text``. Invalid characters are: - unprintable characters - |invalid_filename_chars| - for Windows (or universal) only: |invalid_win_filename_chars| - Replace a value if a sanitized value is a reserved name by operating systems with a specified handler by ``reserved_name_handler``. Args: filename: Filename to sanitize. replacement_text: Replacement text for invalid characters. Defaults to ``""``. platform: Target platform name of the filename. .. include:: platform.txt max_len: Maximum byte length of the ``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. check_reserved: [Deprecated] Use 'reserved_name_handler' instead. null_value_handler: Function called when a value after sanitization is an empty string. You can specify predefined handlers: - :py:func:`~.handler.NullValueHandler.return_null_string` - :py:func:`~.handler.NullValueHandler.return_timestamp` - :py:func:`~.handler.raise_error` Defaults to :py:func:`.handler.NullValueHandler.return_null_string` that just return ``""``. reserved_name_handler: Function called when a value after sanitization is a reserved name. You can specify predefined handlers: - :py:meth:`~.handler.ReservedNameHandler.add_leading_underscore` - :py:meth:`~.handler.ReservedNameHandler.add_trailing_underscore` - :py:meth:`~.handler.ReservedNameHandler.as_is` - :py:func:`~.handler.raise_error` Defaults to :py:func:`.handler.add_trailing_underscore`. additional_reserved_names: Additional reserved names to sanitize. Case insensitive. validate_after_sanitize: Execute validation after sanitization to the file name. Returns: Same type as the ``filename`` (str or PathLike object): Sanitized filename. Raises: ValueError: If the ``filename`` is an invalid filename. Example: :ref:`example-sanitize-filename` """ if check_reserved is not None: warnings.warn( "'check_reserved' is deprecated. Use 'reserved_name_handler' instead.", DeprecationWarning, ) if check_reserved is False: reserved_name_handler = ReservedNameHandler.as_is return FileNameSanitizer( platform=platform, max_len=-1 if max_len is None else max_len, fs_encoding=fs_encoding, null_value_handler=null_value_handler, reserved_name_handler=reserved_name_handler, additional_reserved_names=additional_reserved_names, validate_after_sanitize=validate_after_sanitize, ).sanitize(filename, replacement_text) pathvalidate-3.2.0/pathvalidate/_filepath.py000066400000000000000000000447431450146604500211560ustar00rootroot00000000000000""" .. codeauthor:: Tsuyoshi Hombashi """ import ntpath import os.path import posixpath import re import warnings from pathlib import Path, PurePath from typing import List, Optional, Pattern, Sequence, Tuple from ._base import AbstractSanitizer, AbstractValidator, BaseFile, BaseValidator from ._common import findall_to_str, to_str, validate_pathtype from ._const import _NTFS_RESERVED_FILE_NAMES, DEFAULT_MIN_LEN, INVALID_CHAR_ERR_MSG_TMPL, Platform from ._filename import FileNameSanitizer, FileNameValidator from ._types import PathType, PlatformType from .error import ErrorAttrKey, ErrorReason, InvalidCharError, ReservedNameError, ValidationError from .handler import ReservedNameHandler, ValidationErrorHandler _RE_INVALID_PATH = re.compile(f"[{re.escape(BaseFile._INVALID_PATH_CHARS):s}]", re.UNICODE) _RE_INVALID_WIN_PATH = re.compile(f"[{re.escape(BaseFile._INVALID_WIN_PATH_CHARS):s}]", re.UNICODE) class FilePathSanitizer(AbstractSanitizer): def __init__( self, max_len: int = -1, fs_encoding: Optional[str] = None, platform: Optional[PlatformType] = None, null_value_handler: Optional[ValidationErrorHandler] = None, reserved_name_handler: Optional[ValidationErrorHandler] = None, additional_reserved_names: Optional[Sequence[str]] = None, normalize: bool = True, validate_after_sanitize: bool = False, validator: Optional[AbstractValidator] = None, ) -> None: if validator: fpath_validator = validator else: fpath_validator = FilePathValidator( min_len=DEFAULT_MIN_LEN, max_len=max_len, fs_encoding=fs_encoding, check_reserved=True, additional_reserved_names=additional_reserved_names, platform=platform, ) super().__init__( max_len=max_len, fs_encoding=fs_encoding, validator=fpath_validator, null_value_handler=null_value_handler, reserved_name_handler=reserved_name_handler, additional_reserved_names=additional_reserved_names, platform=platform, validate_after_sanitize=validate_after_sanitize, ) self._sanitize_regexp = self._get_sanitize_regexp() self.__fname_sanitizer = FileNameSanitizer( max_len=self.max_len, fs_encoding=fs_encoding, null_value_handler=null_value_handler, reserved_name_handler=reserved_name_handler, additional_reserved_names=additional_reserved_names, platform=self.platform, validate_after_sanitize=validate_after_sanitize, ) self.__normalize = normalize if self._is_windows(include_universal=True): self.__split_drive = ntpath.splitdrive else: self.__split_drive = posixpath.splitdrive def sanitize(self, value: PathType, replacement_text: str = "") -> PathType: try: validate_pathtype(value, allow_whitespaces=not self._is_windows(include_universal=True)) except ValidationError as e: if e.reason == ErrorReason.NULL_NAME: if isinstance(value, PurePath): raise return self._null_value_handler(e) raise unicode_filepath = to_str(value) drive, unicode_filepath = self.__split_drive(unicode_filepath) unicode_filepath = self._sanitize_regexp.sub(replacement_text, unicode_filepath) if self.__normalize and unicode_filepath: unicode_filepath = os.path.normpath(unicode_filepath) sanitized_path = unicode_filepath sanitized_entries: List[str] = [] if drive: sanitized_entries.append(drive) for entry in sanitized_path.replace("\\", "/").split("/"): if entry in _NTFS_RESERVED_FILE_NAMES: sanitized_entries.append(f"{entry}_") continue sanitized_entry = str( self.__fname_sanitizer.sanitize(entry, replacement_text=replacement_text) ) if not sanitized_entry: if not sanitized_entries: sanitized_entries.append("") continue sanitized_entries.append(sanitized_entry) sanitized_path = self.__get_path_separator().join(sanitized_entries) try: self._validator.validate(sanitized_path) except ValidationError as e: if e.reason == ErrorReason.NULL_NAME: sanitized_path = self._null_value_handler(e) if self._validate_after_sanitize: self._validator.validate(sanitized_path) if isinstance(value, PurePath): return Path(sanitized_path) return sanitized_path def _get_sanitize_regexp(self) -> Pattern[str]: if self._is_windows(include_universal=True): return _RE_INVALID_WIN_PATH return _RE_INVALID_PATH def __get_path_separator(self) -> str: if self._is_windows(): return "\\" return "/" class FilePathValidator(BaseValidator): _RE_NTFS_RESERVED = re.compile( "|".join(f"^/{re.escape(pattern)}$" for pattern in _NTFS_RESERVED_FILE_NAMES), re.IGNORECASE, ) _MACOS_RESERVED_FILE_PATHS = ("/", ":") @property def reserved_keywords(self) -> Tuple[str, ...]: common_keywords = super().reserved_keywords if any([self._is_universal(), self._is_posix(), self._is_macos()]): return common_keywords + self._MACOS_RESERVED_FILE_PATHS if self._is_linux(): return common_keywords + ("/",) return common_keywords def __init__( self, min_len: int = DEFAULT_MIN_LEN, max_len: int = -1, fs_encoding: Optional[str] = None, platform: Optional[PlatformType] = None, check_reserved: bool = True, additional_reserved_names: Optional[Sequence[str]] = None, ) -> None: super().__init__( min_len=min_len, max_len=max_len, fs_encoding=fs_encoding, check_reserved=check_reserved, additional_reserved_names=additional_reserved_names, platform=platform, ) self.__fname_validator = FileNameValidator( min_len=min_len, max_len=max_len, check_reserved=check_reserved, additional_reserved_names=additional_reserved_names, platform=platform, ) if self._is_windows(include_universal=True): self.__split_drive = ntpath.splitdrive else: self.__split_drive = posixpath.splitdrive def validate(self, value: PathType) -> None: validate_pathtype(value, allow_whitespaces=not self._is_windows(include_universal=True)) self.validate_abspath(value) _drive, tail = self.__split_drive(value) if not tail: return unicode_filepath = to_str(tail) byte_ct = len(unicode_filepath.encode(self._fs_encoding)) err_kwargs = { ErrorAttrKey.REASON: ErrorReason.INVALID_LENGTH, ErrorAttrKey.PLATFORM: self.platform, ErrorAttrKey.FS_ENCODING: self._fs_encoding, ErrorAttrKey.BYTE_COUNT: byte_ct, } if byte_ct > self.max_len: raise ValidationError( [ f"file path is too long: expected<={self.max_len:d} bytes, actual={byte_ct:d} bytes" ], **err_kwargs, ) if byte_ct < self.min_len: raise ValidationError( [ "file path is too short: expected>={:d} bytes, actual={:d} bytes".format( self.min_len, byte_ct ) ], **err_kwargs, ) self._validate_reserved_keywords(unicode_filepath) unicode_filepath = unicode_filepath.replace("\\", "/") for entry in unicode_filepath.split("/"): if not entry or entry in (".", ".."): continue self.__fname_validator._validate_reserved_keywords(entry) if self._is_windows(include_universal=True): self.__validate_win_filepath(unicode_filepath) else: self.__validate_unix_filepath(unicode_filepath) def validate_abspath(self, value: PathType) -> None: is_posix_abs = posixpath.isabs(value) is_nt_abs = ntpath.isabs(value) err_object = ValidationError( description=( "an invalid absolute file path ({}) for the platform ({}).".format( value, self.platform.value ) + " to avoid the error, specify an appropriate platform corresponding to" + " the path format or 'auto'." ), platform=self.platform, reason=ErrorReason.MALFORMED_ABS_PATH, ) if any([self._is_windows() and is_nt_abs, self._is_linux() and is_posix_abs]): return if self._is_universal() and any([is_posix_abs, is_nt_abs]): ValidationError( description=( ("POSIX style" if is_posix_abs else "NT style") + " absolute file path found. expected a platform-independent file path." ), platform=self.platform, reason=ErrorReason.MALFORMED_ABS_PATH, ) if self._is_windows(include_universal=True) and is_posix_abs: raise err_object drive, _tail = ntpath.splitdrive(value) if not self._is_windows() and drive and is_nt_abs: raise err_object def __validate_unix_filepath(self, unicode_filepath: str) -> None: match = _RE_INVALID_PATH.findall(unicode_filepath) if match: raise InvalidCharError( INVALID_CHAR_ERR_MSG_TMPL.format( invalid=findall_to_str(match), value=repr(unicode_filepath) ) ) def __validate_win_filepath(self, unicode_filepath: str) -> None: match = _RE_INVALID_WIN_PATH.findall(unicode_filepath) if match: raise InvalidCharError( INVALID_CHAR_ERR_MSG_TMPL.format( invalid=findall_to_str(match), value=repr(unicode_filepath) ), platform=Platform.WINDOWS, ) _drive, value = self.__split_drive(unicode_filepath) if value: match_reserved = self._RE_NTFS_RESERVED.search(value) if match_reserved: reserved_name = match_reserved.group() raise ReservedNameError( f"'{reserved_name}' is a reserved name", reusable_name=False, reserved_name=reserved_name, platform=self.platform, ) def validate_filepath( file_path: PathType, platform: Optional[PlatformType] = None, min_len: int = DEFAULT_MIN_LEN, max_len: Optional[int] = None, fs_encoding: Optional[str] = None, check_reserved: bool = True, additional_reserved_names: Optional[Sequence[str]] = None, ) -> None: """Verifying whether the ``file_path`` is a valid file path or not. Args: file_path (PathType): File path to be validated. platform (Optional[PlatformType], optional): Target platform name of the file path. .. include:: platform.txt min_len (int, optional): Minimum byte length of the ``file_path``. The value must be greater or equal to one. Defaults to ``1``. max_len (Optional[int], optional): Maximum byte length of the ``file_path``. If the value is |None| or minus, automatically determined by the ``platform``: - ``Linux``: 4096 - ``macOS``: 1024 - ``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. check_reserved (bool, optional): If |True|, check reserved names of the ``platform``. Defaults to |True|. additional_reserved_names (Optional[Sequence[str]], optional): Additional reserved names to check. Raises: ValidationError (ErrorReason.INVALID_CHARACTER): If the ``file_path`` includes invalid char(s): |invalid_file_path_chars|. The following characters are also invalid for Windows platforms: |invalid_win_file_path_chars| ValidationError (ErrorReason.INVALID_LENGTH): If the ``file_path`` is longer than ``max_len`` characters. ValidationError: If ``file_path`` include invalid values. Example: :ref:`example-validate-file-path` See Also: `Naming Files, Paths, and Namespaces - Win32 apps | Microsoft Docs `__ """ FilePathValidator( platform=platform, min_len=min_len, max_len=-1 if max_len is None else max_len, fs_encoding=fs_encoding, check_reserved=check_reserved, additional_reserved_names=additional_reserved_names, ).validate(file_path) def is_valid_filepath( file_path: PathType, platform: Optional[PlatformType] = None, min_len: int = DEFAULT_MIN_LEN, max_len: Optional[int] = None, fs_encoding: Optional[str] = None, check_reserved: bool = True, additional_reserved_names: Optional[Sequence[str]] = None, ) -> bool: """Check whether the ``file_path`` is a valid name or not. Args: file_path: A filepath to be checked. platform: Target platform name of the file path. Example: :ref:`example-is-valid-filepath` See Also: :py:func:`.validate_filepath()` """ return FilePathValidator( platform=platform, min_len=min_len, max_len=-1 if max_len is None else max_len, fs_encoding=fs_encoding, check_reserved=check_reserved, additional_reserved_names=additional_reserved_names, ).is_valid(file_path) def sanitize_filepath( file_path: PathType, replacement_text: str = "", platform: Optional[PlatformType] = None, max_len: Optional[int] = None, fs_encoding: Optional[str] = None, check_reserved: Optional[bool] = None, null_value_handler: Optional[ValidationErrorHandler] = None, reserved_name_handler: Optional[ValidationErrorHandler] = None, additional_reserved_names: Optional[Sequence[str]] = None, normalize: bool = True, validate_after_sanitize: bool = False, ) -> PathType: """Make a valid file path from a string. To make a valid file path, the function does the following: - Replace invalid characters for a file path within the ``file_path`` with the ``replacement_text``. Invalid characters are as follows: - unprintable characters - |invalid_file_path_chars| - for Windows (or universal) only: |invalid_win_file_path_chars| - Replace a value if a sanitized value is a reserved name by operating systems with a specified handler by ``reserved_name_handler``. Args: file_path: File path to sanitize. replacement_text: Replacement text for invalid characters. Defaults to ``""``. platform: Target platform name of the file path. .. include:: platform.txt max_len: Maximum byte length of the file path. Truncate the path if the value length exceeds the `max_len`. If the value is |None| or minus, ``max_len`` will automatically determined by the ``platform``: - ``Linux``: 4096 - ``macOS``: 1024 - ``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. check_reserved: [Deprecated] Use 'reserved_name_handler' instead. null_value_handler: Function called when a value after sanitization is an empty string. You can specify predefined handlers: - :py:func:`.handler.NullValueHandler.return_null_string` - :py:func:`.handler.NullValueHandler.return_timestamp` - :py:func:`.handler.raise_error` Defaults to :py:func:`.handler.NullValueHandler.return_null_string` that just return ``""``. reserved_name_handler: Function called when a value after sanitization is one of the reserved names. You can specify predefined handlers: - :py:meth:`~.handler.ReservedNameHandler.add_leading_underscore` - :py:meth:`~.handler.ReservedNameHandler.add_trailing_underscore` - :py:meth:`~.handler.ReservedNameHandler.as_is` - :py:func:`~.handler.raise_error` Defaults to :py:func:`.handler.add_trailing_underscore`. additional_reserved_names: Additional reserved names to sanitize. Case insensitive. normalize: If |True|, normalize the the file path. validate_after_sanitize: Execute validation after sanitization to the file path. Returns: Same type as the argument (str or PathLike object): Sanitized filepath. Raises: ValueError: If the ``file_path`` is an invalid file path. Example: :ref:`example-sanitize-file-path` """ if check_reserved is not None: warnings.warn( "'check_reserved' is deprecated. Use 'reserved_name_handler' instead.", DeprecationWarning, ) if check_reserved is False: reserved_name_handler = ReservedNameHandler.as_is return FilePathSanitizer( platform=platform, max_len=-1 if max_len is None else max_len, fs_encoding=fs_encoding, normalize=normalize, null_value_handler=null_value_handler, reserved_name_handler=reserved_name_handler, additional_reserved_names=additional_reserved_names, validate_after_sanitize=validate_after_sanitize, ).sanitize(file_path, replacement_text) pathvalidate-3.2.0/pathvalidate/_ltsv.py000066400000000000000000000022631450146604500203410ustar00rootroot00000000000000""" .. codeauthor:: Tsuyoshi Hombashi """ import re from ._common import to_str, validate_pathtype from .error import InvalidCharError __RE_INVALID_LTSV_LABEL = re.compile("[^0-9A-Za-z_.-]", re.UNICODE) def validate_ltsv_label(label: str) -> None: """ Verifying whether ``label`` is a valid `Labeled Tab-separated Values (LTSV) `__ label or not. :param label: Label to validate. :raises pathvalidate.ValidationError: If invalid character(s) found in the ``label`` for a LTSV format label. """ validate_pathtype(label, allow_whitespaces=False) match_list = __RE_INVALID_LTSV_LABEL.findall(to_str(label)) if match_list: raise InvalidCharError(f"invalid character found for a LTSV format label: {match_list}") def sanitize_ltsv_label(label: str, replacement_text: str = "") -> str: """ Replace all of the symbols in text. :param label: Input text. :param replacement_text: Replacement text. :return: A replacement string. :rtype: str """ validate_pathtype(label, allow_whitespaces=False) return __RE_INVALID_LTSV_LABEL.sub(replacement_text, to_str(label)) pathvalidate-3.2.0/pathvalidate/_symbol.py000066400000000000000000000044261450146604500206610ustar00rootroot00000000000000""" .. codeauthor:: Tsuyoshi Hombashi """ import re from typing import Sequence from ._common import ascii_symbols, to_str, unprintable_ascii_chars from .error import InvalidCharError __RE_SYMBOL = re.compile( "[{}]".format(re.escape("".join(ascii_symbols + unprintable_ascii_chars))), re.UNICODE ) def validate_symbol(text: str) -> None: """ Verifying whether symbol(s) included in the ``text`` or not. Args: text: Input text to validate. Raises: ValidationError (ErrorReason.INVALID_CHARACTER): If symbol(s) included in the ``text``. """ match_list = __RE_SYMBOL.findall(to_str(text)) if match_list: raise InvalidCharError(f"invalid symbols found: {match_list}") def replace_symbol( text: str, replacement_text: str = "", exclude_symbols: Sequence[str] = [], is_replace_consecutive_chars: bool = False, is_strip: bool = False, ) -> str: """ Replace all of the symbols in the ``text``. Args: text: Input text. replacement_text: Replacement text. exclude_symbols: Symbols that exclude from the replacement. is_replace_consecutive_chars: If |True|, replace consecutive multiple ``replacement_text`` characters to a single character. is_strip: If |True|, strip ``replacement_text`` from the beginning/end of the replacement text. Returns: A replacement string. Example: :ref:`example-sanitize-symbol` """ if exclude_symbols: regexp = re.compile( "[{}]".format( re.escape( "".join(set(ascii_symbols + unprintable_ascii_chars) - set(exclude_symbols)) ) ), re.UNICODE, ) else: regexp = __RE_SYMBOL try: new_text = regexp.sub(replacement_text, to_str(text)) except TypeError: raise TypeError("text must be a string") if not replacement_text: return new_text if is_replace_consecutive_chars: new_text = re.sub(f"{re.escape(replacement_text)}+", replacement_text, new_text) if is_strip: new_text = new_text.strip(replacement_text) return new_text pathvalidate-3.2.0/pathvalidate/_types.py000066400000000000000000000002641450146604500205140ustar00rootroot00000000000000from pathlib import Path from typing import TypeVar from ._const import Platform PathType = TypeVar("PathType", str, Path) PlatformType = TypeVar("PlatformType", str, Platform) pathvalidate-3.2.0/pathvalidate/argparse.py000066400000000000000000000017121450146604500210140ustar00rootroot00000000000000""" .. codeauthor:: Tsuyoshi Hombashi """ from argparse import ArgumentTypeError from ._filename import sanitize_filename, validate_filename from ._filepath import sanitize_filepath, validate_filepath from .error import ValidationError def validate_filename_arg(value: str) -> str: if not value: return "" try: validate_filename(value) except ValidationError as e: raise ArgumentTypeError(e) return value def validate_filepath_arg(value: str) -> str: if not value: return "" try: validate_filepath(value, platform="auto") except ValidationError as e: raise ArgumentTypeError(e) return value def sanitize_filename_arg(value: str) -> str: if not value: return "" return sanitize_filename(value) def sanitize_filepath_arg(value: str) -> str: if not value: return "" return sanitize_filepath(value, platform="auto") pathvalidate-3.2.0/pathvalidate/click.py000066400000000000000000000020651450146604500202770ustar00rootroot00000000000000""" .. codeauthor:: Tsuyoshi Hombashi """ import click from click.core import Context, Option from ._filename import sanitize_filename, validate_filename from ._filepath import sanitize_filepath, validate_filepath from .error import ValidationError def validate_filename_arg(ctx: Context, param: Option, value: str) -> str: if not value: return "" try: validate_filename(value) except ValidationError as e: raise click.BadParameter(str(e)) return value def validate_filepath_arg(ctx: Context, param: Option, value: str) -> str: if not value: return "" try: validate_filepath(value) except ValidationError as e: raise click.BadParameter(str(e)) return value def sanitize_filename_arg(ctx: Context, param: Option, value: str) -> str: if not value: return "" return sanitize_filename(value) def sanitize_filepath_arg(ctx: Context, param: Option, value: str) -> str: if not value: return "" return sanitize_filepath(value) pathvalidate-3.2.0/pathvalidate/error.py000066400000000000000000000165531450146604500203520ustar00rootroot00000000000000""" .. codeauthor:: Tsuyoshi Hombashi """ import enum from typing import Dict, Optional from ._const import Platform def _to_error_code(code: int) -> str: return f"PV{code:04d}" class ErrorAttrKey: BYTE_COUNT = "byte_count" DESCRIPTION = "description" FS_ENCODING = "fs_encoding" PLATFORM = "platform" REASON = "reason" RESERVED_NAME = "reserved_name" REUSABLE_NAME = "reusable_name" @enum.unique class ErrorReason(enum.Enum): """ Validation error reasons. """ NULL_NAME = (_to_error_code(1001), "NULL_NAME", "the value must not be an empty") RESERVED_NAME = ( _to_error_code(1002), "RESERVED_NAME", "found a reserved name by a platform", ) INVALID_CHARACTER = ( _to_error_code(1100), "INVALID_CHARACTER", "invalid characters found", ) INVALID_LENGTH = ( _to_error_code(1101), "INVALID_LENGTH", "found an invalid string length", ) FOUND_ABS_PATH = ( _to_error_code(1200), "FOUND_ABS_PATH", "found an absolute path where must be a relative path", ) MALFORMED_ABS_PATH = ( _to_error_code(1201), "MALFORMED_ABS_PATH", "found a malformed absolute path", ) INVALID_AFTER_SANITIZE = ( _to_error_code(2000), "INVALID_AFTER_SANITIZE", "found invalid value after sanitizing", ) @property def code(self) -> str: """str: Error code.""" return self.__code @property def name(self) -> str: """str: Error reason name.""" return self.__name @property def description(self) -> str: """str: Error reason description.""" return self.__description def __init__(self, code: str, name: str, description: str) -> None: self.__name = name self.__code = code self.__description = description def __str__(self) -> str: return f"[{self.__code}] {self.__description}" class ValidationError(ValueError): """ Exception class of validation errors. """ @property def platform(self) -> Optional[Platform]: """ :py:class:`~pathvalidate.Platform`: Platform information. """ return self.__platform @property def reason(self) -> ErrorReason: """ :py:class:`~pathvalidate.error.ErrorReason`: The cause of the error. """ return self.__reason @property def description(self) -> Optional[str]: """Optional[str]: Error description.""" return self.__description @property def reserved_name(self) -> str: """str: Reserved name.""" return self.__reserved_name @property def reusable_name(self) -> Optional[bool]: """Optional[bool]: Whether the name is reusable or not.""" return self.__reusable_name @property def fs_encoding(self) -> Optional[str]: """Optional[str]: File system encoding.""" return self.__fs_encoding @property def byte_count(self) -> Optional[int]: """Optional[int]: Byte count of the path.""" return self.__byte_count def __init__(self, *args, **kwargs) -> None: # type: ignore if ErrorAttrKey.REASON not in kwargs: raise ValueError(f"{ErrorAttrKey.REASON} must be specified") self.__reason: ErrorReason = kwargs.pop(ErrorAttrKey.REASON) self.__byte_count: Optional[int] = kwargs.pop(ErrorAttrKey.BYTE_COUNT, None) self.__platform: Optional[Platform] = kwargs.pop(ErrorAttrKey.PLATFORM, None) self.__description: Optional[str] = kwargs.pop(ErrorAttrKey.DESCRIPTION, None) self.__reserved_name: str = kwargs.pop(ErrorAttrKey.RESERVED_NAME, "") self.__reusable_name: Optional[bool] = kwargs.pop(ErrorAttrKey.REUSABLE_NAME, None) self.__fs_encoding: Optional[str] = kwargs.pop(ErrorAttrKey.FS_ENCODING, None) try: super().__init__(*args[0], **kwargs) except IndexError: super().__init__(*args, **kwargs) def as_slog(self) -> Dict[str, str]: """Return a dictionary representation of the error. Returns: Dict[str, str]: A dictionary representation of the error. """ slog: Dict[str, str] = { "code": self.reason.code, ErrorAttrKey.DESCRIPTION: self.reason.description, } if self.platform: slog[ErrorAttrKey.PLATFORM] = self.platform.value if self.description: slog[ErrorAttrKey.DESCRIPTION] = self.description if self.__reusable_name is not None: slog[ErrorAttrKey.REUSABLE_NAME] = str(self.__reusable_name) if self.__fs_encoding: slog[ErrorAttrKey.FS_ENCODING] = self.__fs_encoding if self.__byte_count: slog[ErrorAttrKey.BYTE_COUNT] = str(self.__byte_count) return slog def __str__(self) -> str: item_list = [] header = str(self.reason) if Exception.__str__(self): item_list.append(Exception.__str__(self)) if self.platform: item_list.append(f"{ErrorAttrKey.PLATFORM}={self.platform.value}") if self.description: item_list.append(f"{ErrorAttrKey.DESCRIPTION}={self.description}") if self.__reusable_name is not None: item_list.append(f"{ErrorAttrKey.REUSABLE_NAME}={self.reusable_name}") if self.__fs_encoding: item_list.append(f"{ErrorAttrKey.FS_ENCODING}={self.__fs_encoding}") if self.__byte_count is not None: item_list.append(f"{ErrorAttrKey.BYTE_COUNT}={self.__byte_count:,d}") if item_list: header += ": " return header + ", ".join(item_list).strip() def __repr__(self) -> str: return self.__str__() class NullNameError(ValidationError): """[Deprecated] Exception raised when a name is empty. """ def __init__(self, *args, **kwargs) -> None: # type: ignore kwargs[ErrorAttrKey.REASON] = ErrorReason.NULL_NAME super().__init__(args, **kwargs) class InvalidCharError(ValidationError): """ Exception raised when includes invalid character(s) within a string. """ def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] kwargs[ErrorAttrKey.REASON] = ErrorReason.INVALID_CHARACTER super().__init__(args, **kwargs) class ReservedNameError(ValidationError): """ Exception raised when a string matched a reserved name. """ def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] kwargs[ErrorAttrKey.REASON] = ErrorReason.RESERVED_NAME super().__init__(args, **kwargs) class ValidReservedNameError(ReservedNameError): """[Deprecated] Exception raised when a string matched a reserved name. However, it can be used as a name. """ def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] kwargs[ErrorAttrKey.REUSABLE_NAME] = True super().__init__(args, **kwargs) class InvalidReservedNameError(ReservedNameError): """[Deprecated] Exception raised when a string matched a reserved name. Moreover, the reserved name is invalid as a name. """ def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] kwargs[ErrorAttrKey.REUSABLE_NAME] = False super().__init__(args, **kwargs) pathvalidate-3.2.0/pathvalidate/handler.py000066400000000000000000000063041450146604500206270ustar00rootroot00000000000000""" .. codeauthor:: Tsuyoshi Hombashi """ import warnings from datetime import datetime from typing import Callable from .error import ValidationError ValidationErrorHandler = Callable[[ValidationError], str] def return_null_string(e: ValidationError) -> str: """Null value handler that always returns an empty string. Args: e (ValidationError): A validation error. Returns: str: An empty string. """ warnings.warn( "'return_null_string' is deprecated. Use 'NullValueHandler.return_null_string' instead.", DeprecationWarning, ) return "" def return_timestamp(e: ValidationError) -> str: """Null value handler that returns a timestamp of when the function was called. Args: e (ValidationError): A validation error. Returns: str: A timestamp. """ warnings.warn( "'return_timestamp' is deprecated. Use 'NullValueHandler.reserved_name_handler' instead.", DeprecationWarning, ) return str(datetime.now().timestamp()) def raise_error(e: ValidationError) -> str: """Null value handler that always raises an exception. Args: e (ValidationError): A validation error. Raises: ValidationError: Always raised. """ raise e class NullValueHandler: @classmethod def return_null_string(cls, e: ValidationError) -> str: """Null value handler that always returns an empty string. Args: e (ValidationError): A validation error. Returns: str: An empty string. """ return "" @classmethod def return_timestamp(cls, e: ValidationError) -> str: """Null value handler that returns a timestamp of when the function was called. Args: e (ValidationError): A validation error. Returns: str: A timestamp. """ return str(datetime.now().timestamp()) class ReservedNameHandler: @classmethod def add_leading_underscore(cls, e: ValidationError) -> str: """Reserved name handler that adds a leading underscore (``"_"``) to the name except for ``"."`` and ``".."``. Args: e (ValidationError): A reserved name error. Returns: str: The converted name. """ if e.reserved_name in (".", "..") or e.reusable_name: return e.reserved_name return f"_{e.reserved_name}" @classmethod def add_trailing_underscore(cls, e: ValidationError) -> str: """Reserved name handler that adds a trailing underscore (``"_"``) to the name except for ``"."`` and ``".."``. Args: e (ValidationError): A reserved name error. Returns: str: The converted name. """ if e.reserved_name in (".", "..") or e.reusable_name: return e.reserved_name return f"{e.reserved_name}_" @classmethod def as_is(cls, e: ValidationError) -> str: """Reserved name handler that returns the name as is. Args: e (ValidationError): A reserved name error. Returns: str: The name as is. """ return e.reserved_name pathvalidate-3.2.0/pathvalidate/py.typed000066400000000000000000000000001450146604500203220ustar00rootroot00000000000000pathvalidate-3.2.0/pylama.ini000066400000000000000000000007171450146604500161600ustar00rootroot00000000000000[pylama] skip = .eggs/*,.tox/*,*/.env/*,_sandbox/*,build/*,docs/conf.py [pylama:mccabe] max-complexity = 20 [pylama:pycodestyle] max_line_length = 120 [pylama:pylint] max_line_length = 120 [pylama:*/__init__.py] # W0611: imported but unused [pyflakes] ignore = W0611 [pylama:test/test_filename.py] # W605 invalid escape sequence [pycodestyle] ignore = W605,E731 [pylama:test/test_filepath.py] # W605 invalid escape sequence [pycodestyle] ignore = W605,E731 pathvalidate-3.2.0/pyproject.toml000066400000000000000000000026211450146604500171040ustar00rootroot00000000000000[build-system] build-backend = "setuptools.build_meta" requires = ["setuptools>=61.0"] [tool.black] exclude = ''' /( \.eggs | \.git | \.mypy_cache | \.tox | \.pytype | \.venv | _build | buck-out | build | dist )/ | docs/conf.py ''' line-length = 100 target-version = ['py36', 'py37', 'py38', 'py39', 'py310', 'py311'] [tool.isort] include_trailing_comma = true known_third_party = [ 'allpairspy', 'path', 'pytest', 'readmemaker', 'sphinx_rtd_theme', ] line_length = 100 lines_after_imports = 2 multi_line_output = 3 skip_glob = [ '*/.eggs/*', '*/.pytype/*', '*/.tox/*', ] [tool.coverage.run] branch = true source = ['pathvalidate'] [tool.coverage.report] exclude_lines = [ 'except ImportError', 'raise NotImplementedError', 'pass', 'ABCmeta', 'abstractmethod', 'abstractproperty', 'abstractclassmethod', 'warnings.warn', ] precision = 1 show_missing = true [tool.mypy] ignore_missing_imports = true python_version = 3.7 pretty = true check_untyped_defs = true disallow_incomplete_defs = true disallow_untyped_defs = true no_implicit_optional = true show_error_codes = true show_error_context = true warn_redundant_casts = true warn_unreachable = true warn_unused_configs = true warn_unused_ignores = true [tool.pytest.ini_options] testpaths = [ "test", ] md_report = true md_report_color = "auto" md_report_verbose = 0 discord_verbose = 1 pathvalidate-3.2.0/requirements/000077500000000000000000000000001450146604500167125ustar00rootroot00000000000000pathvalidate-3.2.0/requirements/docs_requirements.txt000066400000000000000000000000561450146604500232070ustar00rootroot00000000000000sphinx_rtd_theme>=1.2.2 Sphinx>=2.4 urllib3<2 pathvalidate-3.2.0/requirements/requirements.txt000066400000000000000000000000011450146604500221650ustar00rootroot00000000000000 pathvalidate-3.2.0/requirements/test_requirements.txt000066400000000000000000000001731450146604500232360ustar00rootroot00000000000000allpairspy>=2 click>=6.2 Faker>=1.0.8 pytest>=6.0.1 pytest-discord>=0.1.4; python_version >= '3.7' pytest-md-report>=0.4.1 pathvalidate-3.2.0/setup.py000066400000000000000000000057251450146604500157120ustar00rootroot00000000000000""" .. codeauthor:: Tsuyoshi Hombashi """ import os.path from typing import Dict, Type import setuptools MODULE_NAME = "pathvalidate" REPOSITORY_URL = f"https://github.com/thombashi/{MODULE_NAME:s}" REQUIREMENT_DIR = "requirements" ENCODING = "utf8" pkg_info: Dict[str, str] = {} def get_release_command_class() -> Dict[str, Type[setuptools.Command]]: try: from releasecmd import ReleaseCommand except ImportError: return {} return {"release": ReleaseCommand} with open(os.path.join(MODULE_NAME, "__version__.py")) as f: exec(f.read(), pkg_info) with open("README.rst", encoding=ENCODING) as fp: long_description = fp.read() with open(os.path.join("docs", "pages", "introduction", "summary.txt"), encoding=ENCODING) as f: summary = f.read().strip() with open(os.path.join(REQUIREMENT_DIR, "test_requirements.txt")) as f: TESTS_REQUIRES = [line.strip() for line in f if line.strip()] with open(os.path.join(REQUIREMENT_DIR, "docs_requirements.txt")) as f: docs_requires = [line.strip() for line in f if line.strip()] setuptools.setup( name=MODULE_NAME, version=pkg_info["__version__"], url=REPOSITORY_URL, author=pkg_info["__author__"], author_email=pkg_info["__email__"], description=summary, keywords=["file", "path", "validation", "validator", "sanitization", "sanitizer"], license=pkg_info["__license__"], long_description=long_description, long_description_content_type="text/x-rst", include_package_data=True, packages=setuptools.find_packages(exclude=["test*"]), package_data={MODULE_NAME: ["py.typed"]}, project_urls={ "Documentation": f"https://{MODULE_NAME:s}.rtfd.io/", "Source": REPOSITORY_URL, "Tracker": f"{REPOSITORY_URL:s}/issues", "Changlog": f"{REPOSITORY_URL:s}/releases", }, python_requires=">=3.7", extras_require={ "docs": docs_requires, "test": TESTS_REQUIRES, }, classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Filesystems", "Topic :: Text Processing", ], zip_safe=False, cmdclass=get_release_command_class(), ) pathvalidate-3.2.0/test/000077500000000000000000000000001450146604500151465ustar00rootroot00000000000000pathvalidate-3.2.0/test/__init__.py000066400000000000000000000000001450146604500172450ustar00rootroot00000000000000pathvalidate-3.2.0/test/_common.py000066400000000000000000000035231450146604500171520ustar00rootroot00000000000000""" .. codeauthor:: Tsuyoshi Hombashi """ import random import string from itertools import product alphanum_chars = tuple(x for x in string.digits + string.ascii_letters) INVALID_PATH_CHARS = ("\0",) INVALID_FILENAME_CHARS = ("/",) INVALID_WIN_PATH_CHARS = (":", "*", "?", '"', "<", ">", "|") + INVALID_PATH_CHARS INVALID_WIN_FILENAME_CHARS = INVALID_WIN_PATH_CHARS + INVALID_FILENAME_CHARS + ("\\",) VALID_FILENAME_CHARS = ( "!", "#", "$", "&", "'", "_", "=", "~", "^", "@", "`", "[", "]", "+", "-", ";", "{", "}", ",", ".", "(", ")", "%", ) VALID_PATH_CHARS = VALID_FILENAME_CHARS + ("/",) VALID_PLATFORM_NAMES = ["universal", "linux", "windows", "macos"] INVALID_JS_VAR_CHARS = INVALID_WIN_FILENAME_CHARS + ( "!", "#", "&", "'", "=", "~", "^", "@", "`", "[", "]", "+", "-", ";", "{", "}", ",", ".", "(", ")", "%", " ", "\t", "\n", "\r", "\f", "\v", ) INVALID_PYTHON_VAR_CHARS = INVALID_JS_VAR_CHARS + ("$",) WIN_RESERVED_FILE_NAMES = [ "CON", "con", "PRN", "prn", "AUX", "aux", "CLOCK$", "clock$", "NUL", "nul", ] + [f"{name:s}{num:d}" for name, num in product(["COM", "com", "LPT", "lpt"], range(1, 10))] NTFS_RESERVED_FILE_NAMES = [ "$Mft", "$MftMirr", "$LogFile", "$Volume", "$AttrDef", "$Bitmap", "$Boot", "$BadClus", "$Secure", "$Upcase", "$Extend", "$Quota", "$ObjId", "$Reparse", ] def is_faker_installed(): try: import faker # noqa except ImportError: return False return True def randstr(length, char_list=alphanum_chars): return "".join([random.choice(char_list) for _i in range(length)]) pathvalidate-3.2.0/test/test_argparse.py000066400000000000000000000102561450146604500203670ustar00rootroot00000000000000import platform from argparse import ArgumentError, ArgumentParser import pytest from pathvalidate.argparse import ( sanitize_filename_arg, sanitize_filepath_arg, validate_filename_arg, validate_filepath_arg, ) class Test_validate_filename_arg: @pytest.mark.parametrize( ["value"], [ ["abc"], ["abc.txt"], [""], ], ) def test_normal(self, value): parser = ArgumentParser() parser.add_argument("filename", type=validate_filename_arg) assert parser.parse_args([value]).filename == value @pytest.mark.parametrize( ["value"], [ ["foo/abc"], ["a?c"], ["COM1"], ["a" * 8000], ], ) def test_exception(self, value): parser = ArgumentParser() parser.add_argument("filename", type=validate_filename_arg) try: parser.parse_args([value]) except SystemExit as e: assert isinstance(e.__context__, ArgumentError) else: raise RuntimeError() class Test_validate_filepath_arg: @pytest.mark.parametrize( ["value"], [ ["foo/abc"], ["foo/abc.txt"], [""], ], ) def test_normal(self, value): parser = ArgumentParser() parser.add_argument("filepath", type=validate_filepath_arg) assert parser.parse_args([value]).filepath == value @pytest.mark.skipif(platform.system() == "Windows", reason="platform dependent tests") @pytest.mark.parametrize( ["value"], [ ["a" * 8000], ], ) def test_exception_posix(self, value): parser = ArgumentParser() parser.add_argument("filepath", type=validate_filepath_arg) try: parser.parse_args([value]) except SystemExit as e: assert isinstance(e.__context__, ArgumentError) else: raise RuntimeError() @pytest.mark.skipif(platform.system() != "Windows", reason="platform dependent tests") @pytest.mark.parametrize( ["value"], [ ["foo/a?c"], ["COM1"], ], ) def test_exception_windows(self, value): parser = ArgumentParser() parser.add_argument("filepath", type=validate_filepath_arg) try: parser.parse_args([value]) except SystemExit as e: assert isinstance(e.__context__, ArgumentError) else: raise RuntimeError() class Test_sanitize_filename_arg: @pytest.mark.parametrize( ["value", "expected"], [ ["", ""], ["abc", "abc"], ["abc.txt", "abc.txt"], ["foo/abc", "fooabc"], ["a?c", "ac"], ["COM1", "COM1_"], ], ) def test_normal(self, value, expected): parser = ArgumentParser() parser.add_argument("filename", type=sanitize_filename_arg) assert parser.parse_args([value]).filename == expected class Test_sanitize_filepath_arg: @pytest.mark.skipif(platform.system() == "Windows", reason="platform dependent tests") @pytest.mark.parametrize( ["value", "expected"], [ ["", ""], ["abc", "abc"], ["abc.txt", "abc.txt"], ["foo/abc", "foo/abc"], ["a?c", "a?c"], ["COM1", "COM1"], ], ) def test_normal_posix(self, value, expected): parser = ArgumentParser() parser.add_argument("filepath", type=sanitize_filepath_arg) assert parser.parse_args([value]).filepath == expected @pytest.mark.skipif(platform.system() != "Windows", reason="platform dependent tests") @pytest.mark.parametrize( ["value", "expected"], [ ["", ""], ["abc", "abc"], ["abc.txt", "abc.txt"], ["foo/abc", "foo\\abc"], ["a?c", "ac"], ["COM1", "COM1_"], ], ) def test_normal_windows(self, value, expected): parser = ArgumentParser() parser.add_argument("filepath", type=sanitize_filepath_arg) assert parser.parse_args([value]).filepath == expected pathvalidate-3.2.0/test/test_click.py000066400000000000000000000044251450146604500176510ustar00rootroot00000000000000import click import pytest from click.testing import CliRunner from pathvalidate.click import ( sanitize_filename_arg, sanitize_filepath_arg, validate_filename_arg, validate_filepath_arg, ) @click.command() @click.option("--filename", callback=validate_filename_arg) @click.option("--filepath", callback=validate_filepath_arg) def cli_validate(filename, filepath): if filename: click.echo(filename) if filepath: click.echo(filepath) @click.command() @click.option("--filename", callback=sanitize_filename_arg) @click.option("--filepath", callback=sanitize_filepath_arg) def cli_sanitize(filename, filepath): if filename: click.echo(filename) if filepath: click.echo(filepath) class Test_validate: @pytest.mark.parametrize( ["value", "expected"], [ ["ab", 0], ["", 0], ["a/b", 2], ["a/?b", 2], ], ) def test_normal_filename(self, value, expected): runner = CliRunner() result = runner.invoke(cli_validate, ["--filename", value]) assert result.exit_code == expected @pytest.mark.parametrize( ["value", "expected"], [ ["ab", 0], ["", 0], ["a/b", 0], ["a/?b", 2], ], ) def test_normal_filepath(self, value, expected): runner = CliRunner() result = runner.invoke(cli_validate, ["--filepath", value]) assert result.exit_code == expected class Test_sanitize: @pytest.mark.parametrize( ["value", "expected"], [ ["ab", "ab"], ["", ""], ["a/b", "ab"], ["a/?b", "ab"], ], ) def test_normal_filename(self, value, expected): runner = CliRunner() result = runner.invoke(cli_sanitize, ["--filename", value]) assert result.output.strip() == expected @pytest.mark.parametrize( ["value", "expected"], [ ["ab", "ab"], ["", ""], ["a/b", "a/b"], ["a/?b", "a/b"], ], ) def test_normal_filepath(self, value, expected): runner = CliRunner() result = runner.invoke(cli_sanitize, ["--filepath", value]) assert result.output.strip() == expected pathvalidate-3.2.0/test/test_common.py000066400000000000000000000030411450146604500200450ustar00rootroot00000000000000""" .. codeauthor:: Tsuyoshi Hombashi """ import itertools import pytest from tcolorpy import tcolor from pathvalidate import ( ascii_symbols, replace_ansi_escape, replace_unprintable_char, unprintable_ascii_chars, ) from ._common import alphanum_chars class Test_replace_unprintable_char: TARGET_CHARS = unprintable_ascii_chars NOT_TARGET_CHARS = alphanum_chars + ascii_symbols REPLACE_TEXT_LIST = ["", "_"] @pytest.mark.parametrize( ["value", "replace_text", "expected"], [ ["A" + c + "B", rep, "A" + rep + "B"] for c, rep in itertools.product(TARGET_CHARS, REPLACE_TEXT_LIST) ] + [ ["A" + c + "B", rep, "A" + c + "B"] for c, rep in itertools.product(NOT_TARGET_CHARS, REPLACE_TEXT_LIST) ] + [ ["", "", ""], ], ) def test_normal(self, value, replace_text, expected): assert replace_unprintable_char(value, replace_text) == expected @pytest.mark.parametrize( ["value", "expected"], [ [None, TypeError], [1, TypeError], [True, TypeError], ], ) def test_abnormal(self, value, expected): with pytest.raises(expected): replace_unprintable_char(value) class Test_replace_ansi_escape: def test_normal(self): value = "test" ansi_value = tcolor(value, color="ffffff", bg_color="111111", styles=["bold"]) assert replace_ansi_escape(ansi_value) == value pathvalidate-3.2.0/test/test_error.py000066400000000000000000000025411450146604500177120ustar00rootroot00000000000000import pytest from pathvalidate import Platform from pathvalidate.error import ErrorReason, ValidationError, _to_error_code class Test_to_error_code: @pytest.mark.parametrize( ["value", "expected"], [ [1, "PV0001"], ], ) def test_normal(self, value, expected): assert _to_error_code(value) == expected class Test_str: @pytest.mark.parametrize( ["value", "expected"], [ [ ValidationError( description="hoge", platform=Platform.UNIVERSAL, reason=ErrorReason.INVALID_CHARACTER, ), "[PV1100] invalid characters found: platform=universal, description=hoge", ], ], ) def test_normal(self, value, expected): assert str(value) == expected class Test_as_slog: @pytest.mark.parametrize( ["value", "expected"], [ [ ValidationError( description="hoge", platform=Platform.UNIVERSAL, reason=ErrorReason.INVALID_CHARACTER, ), {"code": "PV1100", "description": "hoge", "platform": "universal"}, ], ], ) def test_normal(self, value, expected): assert value.as_slog() == expected pathvalidate-3.2.0/test/test_filename.py000066400000000000000000000627461450146604500203560ustar00rootroot00000000000000""" .. codeauthor:: Tsuyoshi Hombashi """ import platform as m_platform import random import sys from collections import OrderedDict from itertools import chain, product from pathlib import Path, PurePosixPath, PureWindowsPath import pytest from allpairspy import AllPairs from pathvalidate import ( ErrorReason, Platform, ValidationError, is_valid_filename, sanitize_filename, validate_filename, ) from pathvalidate._common import unprintable_ascii_chars from pathvalidate._filename import FileNameSanitizer, FileNameValidator from pathvalidate.handler import NullValueHandler, ReservedNameHandler, raise_error from ._common import ( INVALID_FILENAME_CHARS, INVALID_PATH_CHARS, INVALID_WIN_FILENAME_CHARS, INVALID_WIN_PATH_CHARS, NTFS_RESERVED_FILE_NAMES, VALID_FILENAME_CHARS, VALID_PLATFORM_NAMES, WIN_RESERVED_FILE_NAMES, is_faker_installed, randstr, ) nan = float("nan") inf = float("inf") random.seed(0) VALID_MULTIBYTE_NAMES = ["新しいテキスト ドキュメント.txt", "新規 Microsoft Excel Worksheet.xlsx"] class Test_FileSanitizer: @pytest.mark.parametrize( ["test_platform", "expected"], [ ["windows", Platform.WINDOWS], ["linux", Platform.LINUX], ["macos", Platform.MACOS], ], ) def test_normal_platform_auto(self, monkeypatch, test_platform, expected): if test_platform == "windows": patch = lambda: "windows" elif test_platform == "linux": patch = lambda: "linux" elif test_platform == "macos": patch = lambda: "macos" else: raise ValueError(f"unexpected test platform: {test_platform}") monkeypatch.setattr(m_platform, "system", patch) assert FileNameSanitizer(255, platform="auto").platform == expected def test_normal_additional_reserved_names(self): sanitizer = FileNameSanitizer(additional_reserved_names=["abc"]) assert sanitizer.reserved_keywords == ("ABC",) class Test_FileNameValidator: @pytest.mark.parametrize( ["test_platform", "expected"], [ [ "windows", ( "AUX", "CLOCK$", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "CON", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", "NUL", "PRN", ), ], ["linux", ()], ["macos", (":",)], ], ) def test_normal_reserved_keywords(self, test_platform, expected): assert FileNameValidator(255, platform=test_platform).reserved_keywords == expected def test_normal_additional_reserved_names(self): sanitizer = FileNameValidator(additional_reserved_names=["abc", "efg.txt"]) assert "ABC" in sanitizer.reserved_keywords assert "EFG.TXT" in sanitizer.reserved_keywords sanitizer = FileNameValidator(platform="windows", additional_reserved_names=["CON"]) assert ( sanitizer.reserved_keywords == FileNameValidator(platform="windows").reserved_keywords ) class Test_validate_filename: VALID_CHARS = VALID_FILENAME_CHARS INVALID_CHARS = INVALID_WIN_FILENAME_CHARS + unprintable_ascii_chars @pytest.mark.parametrize( ["value", "platform"], chain.from_iterable( [ [ args for args in product( ["{0}{1}{0}".format(randstr(64), valid_c)], VALID_PLATFORM_NAMES ) ] for valid_c in VALID_CHARS ] + [ [args for args in product([filename], VALID_PLATFORM_NAMES)] for filename in NTFS_RESERVED_FILE_NAMES ] ), ) def test_normal(self, value, platform): validate_filename(value, platform) assert is_valid_filename(value, platform=platform) @pytest.mark.parametrize( ["platform"], [ ["linux"], ["macos"], ["posix"], ], ) def test_normal_only_whitespaces(self, platform): value = " " validate_filename(value, platform) assert is_valid_filename(value, platform=platform) @pytest.mark.parametrize( ["platform"], [ ["windows"], ["universal"], ], ) def test_abnormal_only_whitespaces(self, platform): value = " " with pytest.raises(ValidationError) as e: validate_filename(value, platform=platform) assert e.value.reason == ErrorReason.NULL assert not is_valid_filename(value, platform) @pytest.mark.parametrize( ["value", "platform"], chain.from_iterable( [ [args for args in product([multibyte_name], VALID_PLATFORM_NAMES)] for multibyte_name in VALID_MULTIBYTE_NAMES ] ), ) def test_normal_multibyte(self, value, platform): validate_filename(value, platform) assert is_valid_filename(value, platform=platform) @pytest.mark.parametrize( ["value", "min_len", "expected"], [ ["lower than one", -1, None], ["valid", 5, None], ["invalid_length", 200, ErrorReason.INVALID_LENGTH], ], ) def test_min_len(self, value, min_len, expected): if expected is None: validate_filename(value, min_len=min_len) assert is_valid_filename(value, min_len=min_len) return with pytest.raises(ValidationError) as e: validate_filename(value, min_len=min_len) assert e.value.reason == expected assert e.value.fs_encoding assert e.value.byte_count > 0 @pytest.mark.parametrize( ["value", "platform", "max_len", "expected"], [ ["a" * 255, None, -1, None], ["a" * 5000, None, 10000, ErrorReason.INVALID_LENGTH], ["valid_length", "universal", 255, None], ["valid_length", Platform.UNIVERSAL, 255, None], ["invalid_length", None, 2, ErrorReason.INVALID_LENGTH], ], ) def test_max_len(self, value, platform, max_len, expected): if expected is None: validate_filename(value, platform=platform, max_len=max_len) assert is_valid_filename(value, platform=platform, max_len=max_len) return with pytest.raises(ValidationError) as e: validate_filename(value, platform=platform, max_len=max_len) assert e.value.reason == expected assert e.value.fs_encoding assert e.value.byte_count > 0 @pytest.mark.parametrize( ["value", "platform", "fs_encoding", "max_len", "expected"], [ ["あ" * 85, "universal", "utf-8", 255, None], ["あ" * 86, "universal", "utf-8", 255, ErrorReason.INVALID_LENGTH], ["あ" * 126, "universal", "utf-16", 255, None], ["あ" * 127, "universal", "utf-16", 255, ErrorReason.INVALID_LENGTH], ], ) def test_max_len_fs_encoding(self, value, platform, fs_encoding, max_len, expected): kwargs = { "platform": platform, "max_len": max_len, "fs_encoding": fs_encoding, } if expected is None: validate_filename(value, **kwargs) assert is_valid_filename(value, **kwargs) return with pytest.raises(ValidationError) as e: validate_filename(value, **kwargs) assert e.value.reason == expected assert e.value.fs_encoding assert e.value.byte_count > 0 @pytest.mark.parametrize( ["value", "min_len", "max_len", "expected"], [ ["minux max_len", 1, -1, None], ["zero max_len", 1, 0, None], ["valid length", 1, 255, None], ["eq min max", 10, 10, None], ["inversion", 100, 1, ValueError], ], ) def test_minmax_len(self, value, min_len, max_len, expected): if expected is None: validate_filename(value, min_len=min_len, max_len=max_len) assert is_valid_filename(value, min_len=min_len, max_len=max_len) return with pytest.raises(expected): validate_filename(value, min_len=min_len, max_len=max_len) @pytest.mark.skipif(not is_faker_installed(), reason="requires faker") @pytest.mark.parametrize(["locale"], [[None], ["ja_JP"]]) def test_locale_ja(self, locale): from faker import Factory fake = Factory.create(locale=locale, seed=1) for _ in range(100): filename = fake.file_name() validate_filename(filename) assert is_valid_filename(filename) @pytest.mark.parametrize( ["value", "platform"], chain.from_iterable( [ [ args for args in product( ["{0}{1}{0}".format(randstr(64), invalid_c)], VALID_PLATFORM_NAMES ) ] for invalid_c in INVALID_FILENAME_CHARS ] ), ) def test_exception_invalid_char(self, value, platform): with pytest.raises(ValidationError) as e: validate_filename(value, platform) assert e.value.reason == ErrorReason.INVALID_CHARACTER assert not is_valid_filename(value, platform=platform) @pytest.mark.parametrize( ["value", "platform"], [ ["a/b", Platform.UNIVERSAL], ["a*b", Platform.WINDOWS], [PurePosixPath("a/b"), Platform.UNIVERSAL], [PureWindowsPath("a/b"), Platform.WINDOWS], ], ) def test_exception_invalid_char_specific_target_platform(self, value, platform): with pytest.raises(ValidationError) as e: validate_filename(value, platform) assert e.value.reason == ErrorReason.INVALID_CHARACTER assert e.value.platform == platform @pytest.mark.parametrize( ["value", "platform"], [ ["{0}{1}{0}".format(randstr(64), invalid_c), platform] for invalid_c, platform in product( set(INVALID_WIN_PATH_CHARS).difference( set(INVALID_PATH_CHARS + INVALID_FILENAME_CHARS + unprintable_ascii_chars) ), ["windows", "universal"], ) ], ) def test_exception_win_invalid_char(self, value, platform): with pytest.raises(ValidationError) as e: validate_filename(value, platform=platform) assert e.value.reason == ErrorReason.INVALID_CHARACTER assert not is_valid_filename(value, platform=platform) @pytest.mark.parametrize( ["value", "platform", "expected"], [ [reserved_keyword, platform, ValidationError] for reserved_keyword, platform in product( WIN_RESERVED_FILE_NAMES, ["windows", "universal"] ) ] + [ [f"{reserved_keyword}.txt", platform, ValidationError] for reserved_keyword, platform in product( WIN_RESERVED_FILE_NAMES, ["windows", "universal"] ) ] + [ [reserved_keyword, platform, None] for reserved_keyword, platform in product([".", ".."], ["posix", "linux", "macos"]) ] + [ [":", "posix", ValidationError], [":", "macos", ValidationError], ], ) def test_reserved_name(self, value, platform, expected): if expected is None: validate_filename(value, platform=platform) else: with pytest.raises(expected) as e: validate_filename(value, platform=platform) assert e.value.reason == ErrorReason.RESERVED_NAME assert e.value.reserved_name assert e.value.reusable_name is False assert not is_valid_filename(value, platform=platform) @pytest.mark.parametrize( ["value", "arn", "expected"], [ ["abc", [], True], ["abc", ["abc"], False], ["Abc", ["abc"], False], ["ABC", ["abc"], False], ["abc.txt", ["abc.txt"], False], ], ) def test_normal_additional_reserved_names(self, value, arn, expected): assert is_valid_filename(value, additional_reserved_names=arn) == expected @pytest.mark.parametrize( ["platform", "value", "expected"], [ [win_abspath, platform, None] for win_abspath, platform in product( ["linux", "macos", "posix"], ["\\", "\\\\", "\\ ", "C:\\", "c:\\", "\\xyz", "\\xyz "], ) ] + [ [win_abspath, platform, ValidationError] for win_abspath, platform in product( ["windows", "universal"], ["\\", "\\\\", "\\ ", "C:\\", "c:\\", "\\xyz", "\\xyz "] ) ], ) def test_win_abs_path(self, platform, value, expected): if expected is None: validate_filename(value, platform=platform) else: with pytest.raises(expected) as e: validate_filename(value, platform=platform) assert e.value.reason == ErrorReason.FOUND_ABS_PATH @pytest.mark.parametrize( ["value", "platform"], [ [value, platform] for value, platform in product( ["a/b.txt", "/a/b.txt", "c:\\Users"], ["windows", "universal"] ) ], ) def test_exception_filepath(self, value, platform): with pytest.raises(ValidationError) as e: validate_filename(value, platform=platform) assert e.value.reason in [ErrorReason.FOUND_ABS_PATH, ErrorReason.INVALID_CHARACTER] assert not is_valid_filename(value, platform=platform) @pytest.mark.parametrize( ["value", "platform", "expected"], [ [value, platform, ErrorReason.INVALID_CHARACTER] for value, platform in product(["asdf\rsdf"], ["windows", "universal"]) ], ) def test_exception_escape_err_msg(self, value, platform, expected): with pytest.raises(ValidationError) as e: print(platform, repr(value)) validate_filename(value, platform=platform) assert e.value.reason == ErrorReason.INVALID_CHARACTER assert str(e.value) == ( r"[PV1100] invalid characters found: invalids=('\r'), value='asdf\rsdf', " "platform=Windows" ) # noqa @pytest.mark.parametrize( ["value", "expected"], [ [None, ValueError], ["", ValidationError], ], ) def test_exception_null_value(self, value, expected): with pytest.raises(expected): validate_filename(value) assert not is_valid_filename(value) @pytest.mark.parametrize( ["value", "expected"], [ ["a" * 256, ValidationError], [1, TypeError], [True, TypeError], [nan, TypeError], [inf, TypeError], ], ) def test_exception(self, value, expected): with pytest.raises(expected): validate_filename(value) assert not is_valid_filename(value) class Test_sanitize_filename: SANITIZE_CHARS = INVALID_WIN_FILENAME_CHARS + unprintable_ascii_chars NOT_SANITIZE_CHARS = VALID_FILENAME_CHARS REPLACE_TEXT_LIST = ["", "_"] @pytest.mark.parametrize( ["platform", "value", "replace_text", "expected"], [ ["universal", "A" + c + "B", rep, "A" + rep + "B"] for c, rep in product( INVALID_WIN_FILENAME_CHARS + unprintable_ascii_chars, REPLACE_TEXT_LIST ) ] + [ ["universal", "A" + c + "B", rep, "A" + c + "B"] for c, rep in product(NOT_SANITIZE_CHARS, REPLACE_TEXT_LIST) ] + [ [pair.platform, "A" + pair.c + "B", pair.repl, "A" + pair.repl + "B"] for pair in AllPairs( OrderedDict( { "platform": [ "posix", "linux", "macos", Platform.POSIX, Platform.LINUX, Platform.MACOS, ], "c": INVALID_PATH_CHARS + unprintable_ascii_chars, "repl": REPLACE_TEXT_LIST, } ) ) ] + [ [pair.platform, "A" + pair.c + "B", pair.repl, "A" + pair.c + "B"] for pair in AllPairs( OrderedDict( { "platform": [ "posix", "linux", "macos", Platform.POSIX, Platform.LINUX, Platform.MACOS, ], "c": [":", "*", "?", '"', "<", ">", "|"], "repl": REPLACE_TEXT_LIST, } ) ) ], ) def test_normal_str(self, platform, value, replace_text, expected): sanitized_name = sanitize_filename(value, platform=platform, replacement_text=replace_text) assert sanitized_name == expected assert isinstance(sanitized_name, str) validate_filename(sanitized_name, platform=platform) assert is_valid_filename(sanitized_name, platform=platform) @pytest.mark.parametrize( ["value", "replace_text", "expected"], [ [Path("A" + c + "B"), rep, Path("A" + rep + "B")] for c, rep in product(SANITIZE_CHARS, REPLACE_TEXT_LIST) ] + [ [Path("A" + c + "B"), rep, Path("A" + c + "B")] for c, rep in product(NOT_SANITIZE_CHARS, REPLACE_TEXT_LIST) ], ) def test_normal_pathlike(self, value, replace_text, expected): sanitized_name = sanitize_filename(value, replace_text) assert sanitized_name == expected assert isinstance(sanitized_name, Path) validate_filename(sanitized_name) assert is_valid_filename(sanitized_name) @pytest.mark.parametrize( ["value"], [ [None], [""], ["/"], ["//"], ["?"], ], ) def test_normal_null_value_handler(self, value): assert ( sanitize_filename(value, null_value_handler=NullValueHandler.return_null_string) == "" ) assert sanitize_filename(value, null_value_handler=NullValueHandler.return_timestamp) != "" with pytest.raises(ValidationError): sanitize_filename(value, null_value_handler=raise_error) @pytest.mark.parametrize( ["value", "replace_text", "expected"], [ ["あい/うえお.txt", "", "あいうえお.txt"], ["属/性.txt", "-", "属-性.txt"], ], ) def test_normal_multibyte(self, value, replace_text, expected): sanitized_name = sanitize_filename(value, replace_text) assert sanitized_name == expected validate_filename(sanitized_name) assert is_valid_filename(sanitized_name) @pytest.mark.parametrize( ["value", "max_len", "expected"], [ ["a" * 10, 255, 10], ["invalid_length" * 100, 255, 255], ["invalid_length" * 100, 10, 10], ], ) def test_normal_max_len(self, value, max_len, expected): filename = sanitize_filename(value, max_len=max_len) assert len(filename) == expected assert is_valid_filename(filename, max_len=max_len) @pytest.mark.parametrize( ["value", "test_platform", "expected"], [ [reserved.lower(), "windows", reserved.lower() + "_"] for reserved in WIN_RESERVED_FILE_NAMES ] + [ [f"{reserved_keyword}.txt", platform, f"{reserved_keyword}_.txt"] for reserved_keyword, platform in product( WIN_RESERVED_FILE_NAMES, ["windows", "universal"] ) if reserved_keyword not in [".", ".."] ] + [ [reserved.upper(), "windows", reserved.upper() + "_"] for reserved in WIN_RESERVED_FILE_NAMES ] + [ [reserved_keyword, platform, reserved_keyword] for reserved_keyword, platform in product([".", ".."], ["windows", "universal"]) ], ) def test_normal_reserved_name(self, value, test_platform, expected): filename = sanitize_filename(value, platform=test_platform) assert filename == expected assert is_valid_filename(filename, platform=test_platform) @pytest.mark.parametrize( ["value", "reserved_name_handler", "expected"], [ ["CON", ReservedNameHandler.add_trailing_underscore, "CON_"], ["CON", ReservedNameHandler.add_leading_underscore, "_CON"], ["CON", ReservedNameHandler.as_is, "CON"], ], ) def test_normal_reserved_name_handler(self, value, reserved_name_handler, expected): for platform in ["windows", "universal"]: assert ( sanitize_filename( value, platform=platform, reserved_name_handler=reserved_name_handler ) == expected ) def test_exception_reserved_name_handler(self): for platform in ["windows", "universal"]: with pytest.raises(ValidationError) as e: sanitize_filename("CON", platform=platform, reserved_name_handler=raise_error) assert e.value.reason == ErrorReason.RESERVED_NAME @pytest.mark.parametrize( ["value", "arn", "expected"], [ ["abc", [], "abc"], ["abc", ["abc"], "abc_"], ], ) def test_normal_additional_reserved_names(self, value, arn, expected): for platform in ["windows", "universal"]: assert ( sanitize_filename( value, platform=platform, additional_reserved_names=arn, ) == expected ) @pytest.mark.parametrize( ["value", "check_reserved", "expected"], [ ["CON", True, "CON_"], ["CON", False, "CON"], ], ) def test_normal_check_reserved(self, value, check_reserved, expected): for platform in ["windows", "universal"]: assert ( sanitize_filename(value, platform=platform, check_reserved=check_reserved) == expected ) @pytest.mark.parametrize( ["platform", "value", "expected"], [ ["windows", "period.", "period"], ["windows", "space ", "space"], ["windows", " space ", "space"], ["windows", "space_and_period .", "space_and_period"], ["windows", "space_and_period. ", "space_and_period"], ["windows", " .space_and_period", ".space_and_period"], ["windows", ". space_and_period", ". space_and_period"], ["windows", ". ", "."], ["windows", " .", "."], ["windows", " . ", "."], ["windows", ".. ", ".."], ["linux", "period.", "period."], ["linux", "space ", "space "], ["linux", "space_and_period. ", "space_and_period. "], ["universal", "period.", "period"], ["universal", "space ", "space"], ["universal", "space_and_period .", "space_and_period"], ["universal", ". ", "."], ["universal", " .", "."], ["universal", " . ", "."], ["universal", ".. ", ".."], ], ) def test_normal_space_or_period_at_tail(self, platform, value, expected): filename = sanitize_filename(value, platform=platform) assert filename == expected assert is_valid_filename(filename, platform=platform) @pytest.mark.parametrize( ["platform", "value"], [ [platform, value] for platform, value in product( ["windows", "universal"], [ "\a\r", ], ) ], ) def test_exception_invalid_after_sanitize(self, platform, value): kwargs = { "platform": platform, "replacement_text": "", "validate_after_sanitize": False, } print(f"'{sanitize_filename(value, **kwargs)}'", file=sys.stderr) kwargs["validate_after_sanitize"] = True with pytest.raises(ValidationError) as e: sanitize_filename(value, **kwargs) assert e.value.reason == ErrorReason.INVALID_AFTER_SANITIZE @pytest.mark.parametrize( ["value", "expected"], [ [1, TypeError], [True, TypeError], ], ) def test_exception_type(self, value, expected): with pytest.raises(expected): sanitize_filename(value) assert not is_valid_filename(value) pathvalidate-3.2.0/test/test_filepath.py000066400000000000000000000774741450146604500203760ustar00rootroot00000000000000""" .. codeauthor:: Tsuyoshi Hombashi """ import platform as m_platform import random import sys from collections import OrderedDict from itertools import chain, product from pathlib import Path import pytest from allpairspy import AllPairs from pathvalidate import ( ErrorReason, Platform, ValidationError, is_valid_filepath, sanitize_filepath, validate_filepath, ) from pathvalidate._common import unprintable_ascii_chars from pathvalidate._filepath import FilePathSanitizer, FilePathValidator from pathvalidate.handler import NullValueHandler, ReservedNameHandler, raise_error from ._common import ( INVALID_PATH_CHARS, INVALID_WIN_PATH_CHARS, NTFS_RESERVED_FILE_NAMES, VALID_PATH_CHARS, WIN_RESERVED_FILE_NAMES, is_faker_installed, randstr, ) nan = float("nan") inf = float("inf") random.seed(0) class Test_FileSanitizer: @pytest.mark.parametrize( ["test_platform", "expected"], [ ["windows", Platform.WINDOWS], ["linux", Platform.LINUX], ["macos", Platform.MACOS], ], ) def test_normal_platform_auto(self, monkeypatch, test_platform, expected): if test_platform == "windows": patch = lambda: "windows" elif test_platform == "linux": patch = lambda: "linux" elif test_platform == "macos": patch = lambda: "macos" else: raise ValueError(f"unexpected test platform: {test_platform}") monkeypatch.setattr(m_platform, "system", patch) assert FilePathSanitizer(255, platform="auto").platform == expected def test_normal_additional_reserved_names(self): sanitizer = FilePathSanitizer(additional_reserved_names=["abc"]) assert sanitizer.reserved_keywords == ("ABC",) class Test_FilePathValidator: @pytest.mark.parametrize( ["test_platform", "expected"], [ ["windows", tuple()], ["posix", ("/", ":")], ["linux", ("/",)], ["macos", ("/", ":")], ], ) def test_normal_reserved_keywords(self, test_platform, expected): assert FilePathValidator(255, platform=test_platform).reserved_keywords == expected def test_normal_additional_reserved_names(self): sanitizer = FilePathValidator(additional_reserved_names=["abc"]) assert "ABC" in sanitizer.reserved_keywords class Test_validate_filepath: VALID_CHARS = VALID_PATH_CHARS VALID_MULTIBYTE_PATHS = [ "c:\\Users\\新しいフォルダー\\あいうえお.txt", "D:\\新しいフォルダー\\ユーザ属性.txt", ] WIN_VALID_PATHS = [ r"C:\Program Files (x86)\Microsoft", "D:\\Users\\\\est\\AppData\\Local\\Temp\\pytest-of-test\\pytest-0\\\\hoge.csv", "D:/Users/test/AppData/Local/Temp/pytest-of-test/pytest-0/test_exception__hoge_csv_heade1/hoge.csv", # noqa "C:\\Users\\est\\AppData/Local\\Temp/pytest-of-test\\pytest-0/\\hoge.csv", "C:\\Users", "C:\\", "\\Users", ] @pytest.mark.parametrize( ["value", "platform"], chain.from_iterable( [ [ args for args in product( ["/{0}/{1}{0}".format(randstr(64), valid_c)], ["linux", "macos"] ) ] for valid_c in VALID_CHARS ] ), ) def test_normal(self, value, platform): validate_filepath(value, platform) assert is_valid_filepath(value, platform=platform) @pytest.mark.parametrize( ["platform"], [ ["linux"], ["macos"], ["posix"], ], ) def test_normal_only_whitespaces(self, platform): value = " " validate_filepath(value, platform) assert is_valid_filepath(value, platform=platform) @pytest.mark.parametrize( ["platform"], [ ["windows"], ["universal"], ], ) def test_abnormal_only_whitespaces(self, platform): value = " " with pytest.raises(ValidationError) as e: validate_filepath(value, platform=platform) assert e.value.reason == ErrorReason.NULL assert not is_valid_filepath(value, platform) @pytest.mark.parametrize( ["value", "platform"], chain.from_iterable( [ [args for args in product([valid_path], ["windows"])] for valid_path in VALID_MULTIBYTE_PATHS ] ), ) def test_normal_multibyte(self, value, platform): validate_filepath(value, platform) assert is_valid_filepath(value, platform=platform) @pytest.mark.parametrize(["value"], [[valid_path] for valid_path in WIN_VALID_PATHS]) def test_normal_win(self, value): platform = "windows" validate_filepath(value, platform=platform) assert is_valid_filepath(value, platform=platform) @pytest.mark.parametrize( ["value", "min_len", "expected"], [ ["lower than one", -1, None], ["valid", 5, None], ["invalid_length", 200, ErrorReason.INVALID_LENGTH], ], ) def test_normal_min_len(self, value, min_len, expected): if expected is None: validate_filepath(value, min_len=min_len) assert is_valid_filepath(value, min_len=min_len) return with pytest.raises(ValidationError) as e: validate_filepath(value, min_len=min_len) assert e.value.reason == expected assert e.value.fs_encoding assert e.value.byte_count > 0 @pytest.mark.parametrize( ["value", "platform", "max_len", "expected"], [ ["a" * 4096, "linux", None, None], ["a" * 4097, "linux", None, ErrorReason.INVALID_LENGTH], ["a" * 1024, "posix", None, None], ["a" * 1025, "posix", None, ErrorReason.INVALID_LENGTH], ["a" * 4097, Platform.LINUX, None, ErrorReason.INVALID_LENGTH], ["a" * 255, "linux", 100, ErrorReason.INVALID_LENGTH], ["a" * 5000, "windows", 10000, ErrorReason.INVALID_LENGTH], ["a" * 260, "windows", None, None], ["a" * 300, "windows", 1024, ErrorReason.INVALID_LENGTH], ["a" * 261, Platform.WINDOWS, None, ErrorReason.INVALID_LENGTH], ["a" * 261, "windows", None, ErrorReason.INVALID_LENGTH], ["a" * 260, "universal", None, None], ["a" * 261, "universal", None, ErrorReason.INVALID_LENGTH], ["a" * 300, "universal", 1024, ErrorReason.INVALID_LENGTH], ["a" * 261, Platform.UNIVERSAL, None, ErrorReason.INVALID_LENGTH], ], ) def test_normal_max_len(self, value, platform, max_len, expected): kwargs = { "platform": platform, "max_len": max_len, } if expected is None: validate_filepath(value, **kwargs) assert is_valid_filepath(value, **kwargs) return with pytest.raises(ValidationError) as e: validate_filepath(value, **kwargs) assert e.value.reason == ErrorReason.INVALID_LENGTH assert e.value.fs_encoding assert e.value.byte_count > 0 @pytest.mark.parametrize( ["value", "platform", "fs_encoding", "max_len", "expected"], [ ["/tmp/" + "あ" * 83, "linux", "utf-8", 255, None], ["/tmp/" + "あ" * 84, "linux", "utf-8", 255, ErrorReason.INVALID_LENGTH], ["/tmp/" + "あ" * 121, "linux", "utf-16", 255, None], ["/tmp/" + "あ" * 122, "linux", "utf-16", 255, ErrorReason.INVALID_LENGTH], ], ) def test_max_len_fs_encoding(self, value, platform, fs_encoding, max_len, expected): kwargs = { "platform": platform, "max_len": max_len, "fs_encoding": fs_encoding, } if expected is None: validate_filepath(value, **kwargs) assert is_valid_filepath(value, **kwargs) return with pytest.raises(ValidationError) as e: validate_filepath(value, **kwargs) assert e.value.reason == expected assert e.value.fs_encoding assert e.value.byte_count > 0 @pytest.mark.parametrize( ["value", "min_len", "max_len", "expected"], [ ["valid length", 1, 255, None], ["minus min_len", -2, 100, None], ["minus max_len", -3, -2, None], ["zero max_len", -2, 0, None], ["eq min max", 10, 10, None], ["inversion", 100, 1, ValueError], ], ) def test_minmax_len(self, value, min_len, max_len, expected): kwargs = { "min_len": min_len, "max_len": max_len, } if expected is None: validate_filepath(value, **kwargs) assert is_valid_filepath(value, **kwargs) return with pytest.raises(expected): validate_filepath(value, **kwargs) @pytest.mark.parametrize( ["test_platform", "value", "expected"], [ ["linux", "/a/b/c.txt", None], ["linux", "C:\\a\\b\\c.txt", ValidationError], ["windows", "/a/b/c.txt", None], ["windows", "C:\\a\\b\\c.txt", None], ["universal", "/a/b/c.txt", ValidationError], ["universal", "C:\\a\\b\\c.txt", ValidationError], ], ) def test_abs_path(self, test_platform, value, expected): if expected is None: validate_filepath(value, platform=test_platform) assert is_valid_filepath(value, platform=test_platform) return with pytest.raises(expected): validate_filepath(value, platform=test_platform) @pytest.mark.skipif(m_platform.system() != "Windows", reason="platform dependent tests") @pytest.mark.parametrize( ["value", "expected"], [ ["C:\\a\\b\\c.txt", None], ], ) def test_auto_platform_win(self, value, expected): if expected is None: validate_filepath(value, platform="auto") assert is_valid_filepath(value, platform="auto") return with pytest.raises(expected): validate_filepath(value, platform="auto") @pytest.mark.skipif(m_platform.system() != "Linux", reason="platform dependent tests") @pytest.mark.parametrize( ["value", "expected"], [ ["/a/b/c.txt", None], ["C:\\a\\b\\c.txt", ValidationError], ], ) def test_auto_platform_linux(self, value, expected): if expected is None: validate_filepath(value, platform="auto") assert is_valid_filepath(value, platform="auto") return with pytest.raises(expected): validate_filepath(value, platform="auto") @pytest.mark.parametrize( ["test_platform", "value", "expected"], [ ["linux", "a/b/c.txt", None], ["linux", "a//b?/c.txt", None], ["linux", "../a/./../b/c.txt", None], ["windows", "a/b/c.txt", None], ["windows", "a//b?/c.txt", ValidationError], ["windows", "../a/./../b/c.txt", None], ["universal", "a/b/c.txt", None], ["universal", "./a/b/c.txt", None], ["universal", "../a/./../b/c.txt", None], ["universal", "a//b?/c.txt", ValidationError], ], ) def test_relative_path(self, test_platform, value, expected): if expected is None: validate_filepath(value, platform=test_platform) assert is_valid_filepath(value, platform=test_platform) else: with pytest.raises(expected): validate_filepath(value, platform=test_platform) @pytest.mark.parametrize( ["platform", "value"], [ ["windows", "period."], ["windows", "space "], ["windows", "space_and_period ."], ["windows", "space_and_period. "], ["linux", "period."], ["linux", "space "], ["linux", "space_and_period. "], ["universal", "period."], ["universal", "space "], ["universal", "space_and_period ."], ], ) def test_normal_space_or_period_at_tail(self, platform, value): validate_filepath(value, platform=platform) assert is_valid_filepath(value, platform=platform) @pytest.mark.skipif(not is_faker_installed(), reason="requires faker") @pytest.mark.parametrize(["locale"], [[None], ["ja_JP"]]) def test_locale_jp(self, locale): from faker import Factory fake = Factory.create(locale=locale, seed=1) for _ in range(100): filepath = fake.file_path() validate_filepath(filepath, platform="linux") assert is_valid_filepath(filepath, platform="linux") @pytest.mark.parametrize( ["value"], [["{0}{1}{0}".format(randstr(64), invalid_c)] for invalid_c in INVALID_PATH_CHARS], ) def test_exception_invalid_char(self, value): with pytest.raises(ValidationError) as e: validate_filepath(value) assert e.value.reason == ErrorReason.INVALID_CHARACTER assert not is_valid_filepath(value) @pytest.mark.parametrize( ["value", "platform"], [ ["{0}/{1}{0}".format(randstr(64), invalid_c), platform] for invalid_c, platform in product( set(INVALID_WIN_PATH_CHARS + unprintable_ascii_chars).difference( set(INVALID_PATH_CHARS) ), ["windows", "universal"], ) ], ) def test_exception_invalid_win_char(self, value, platform): with pytest.raises(ValidationError) as e: validate_filepath(value, platform=platform) assert e.value.reason == ErrorReason.INVALID_CHARACTER assert not is_valid_filepath(value, platform=platform) @pytest.mark.parametrize( ["value", "platform"], [ [f"/foo/abc/{reserved_keyword}.txt", platform] for reserved_keyword, platform in product(WIN_RESERVED_FILE_NAMES, ["linux", "macos"]) if reserved_keyword not in [".", ".."] ] + [ [f"{drive}\\{filename}_", platform] for drive, platform, filename in product( ["C:", "D:"], ["windows"], NTFS_RESERVED_FILE_NAMES ) ] + [ [f"{drive}\\abc\\{filename}", platform] for drive, platform, filename in product( ["C:", "D:"], ["windows"], NTFS_RESERVED_FILE_NAMES ) ], ) def test_normal_reserved_name_used_valid_place(self, value, platform): validate_filepath(value, platform=platform) assert is_valid_filepath(value, platform=platform) @pytest.mark.parametrize( ["value", "platform", "expected"], [ [f"abc\\{reserved_keyword}\\xyz", platform, ValidationError] for reserved_keyword, platform in product( WIN_RESERVED_FILE_NAMES, ["windows", "universal"] ) if reserved_keyword not in [".", ".."] ] + [ [f"foo/abc/{reserved_keyword}.txt", platform, ValidationError] for reserved_keyword, platform in product(WIN_RESERVED_FILE_NAMES, ["universal"]) if reserved_keyword not in [".", ".."] ] + [ [f"{reserved_keyword}", platform, ValidationError] for reserved_keyword, platform in product( WIN_RESERVED_FILE_NAMES, ["windows", "universal"] ) if reserved_keyword not in [".", ".."] ] + [ [f"{drive}\\{filename}", platform, ValidationError] for drive, platform, filename in product( ["C:", "D:"], ["windows"], NTFS_RESERVED_FILE_NAMES ) ], ) def test_exception_reserved_name(self, value, platform, expected): with pytest.raises(expected) as e: print(platform, value) validate_filepath(value, platform=platform) assert e.value.reason == ErrorReason.RESERVED_NAME assert e.value.reusable_name is False assert e.value.reserved_name assert not is_valid_filepath(value, platform=platform) @pytest.mark.parametrize( ["value", "arn", "expected"], [ ["abc/efg.txt", ["abc"], False], ["abc/efg.txt", ["efg.txt"], False], ], ) def test_normal_additional_reserved_names(self, value, arn, expected): assert is_valid_filepath(value, additional_reserved_names=arn) == expected @pytest.mark.parametrize( ["value", "platform", "expected"], [ [value, platform, ErrorReason.INVALID_CHARACTER] for value, platform in product(["asdf\rsdf"], ["windows", "universal"]) ], ) def test_exception_escape_err_msg(self, value, platform, expected): with pytest.raises(ValidationError) as e: print(platform, repr(value)) validate_filepath(value, platform=platform) assert e.value.reason == expected assert str(e.value) == ( r"[PV1100] invalid characters found: invalids=('\r'), value='asdf\rsdf', " "platform=Windows" ) # noqa @pytest.mark.parametrize( ["value", "expected"], [ [None, ValueError], ["", ValidationError], [1, TypeError], [True, TypeError], ], ) def test_exception(self, value, expected): with pytest.raises(expected): validate_filepath(value) assert not is_valid_filepath(value) class Test_validate_win_file_path: VALID_CHARS = VALID_PATH_CHARS @pytest.mark.parametrize( ["value"], [ ["C:\\Users\\est\\AppData\\Local\\Temp\\pytest-of-test\\pytest-0\\\\hoge.csv"], ["Z:\\Users\\est\\AppData\\Local\\Temp\\pytest-of-test\\pytest-0\\hoge.csv"], [ "C:/Users/est/AppData/Local/Temp/pytest-of-test/pytest-0/test_exception__hoge_csv_heade1/hoge.csv" # noqa ], ["C:\\Users/est\\AppData/Local\\Temp/pytest-of-test\\pytest-0/hoge.csv"], ["C:\\Users"], ["C:\\"], ["\\Users"], ], ) def test_normal(self, value): validate_filepath(value, platform="windows") assert is_valid_filepath(value, platform="windows") @pytest.mark.parametrize( ["value", "platform", "expected"], [ [r"C:\Users\a", "universal", ErrorReason.MALFORMED_ABS_PATH], [r"C:\Users:a", "universal", ErrorReason.MALFORMED_ABS_PATH], ["C:\\Users\\" + "a" * 1024, "windows", ErrorReason.INVALID_LENGTH], [r"C:\Users:a", "windows", ErrorReason.INVALID_CHARACTER], ], ) def test_exception(self, value, platform, expected): with pytest.raises(ValidationError) as e: validate_filepath(value, platform=platform) assert e.value.reason == expected assert not is_valid_filepath(value, platform=platform) class Test_sanitize_filepath: SANITIZE_CHARS = INVALID_WIN_PATH_CHARS + unprintable_ascii_chars NOT_SANITIZE_CHARS = VALID_PATH_CHARS REPLACE_TEXTS = ["", "_"] @pytest.mark.parametrize( ["platform", "value", "replace_text", "expected"], [ ["universal", "AA" + c + "B", rep, "AA" + rep + "B"] for c, rep in product(SANITIZE_CHARS, REPLACE_TEXTS) ] + [ ["universal", "A" + c + "B", rep, "A" + c + "B"] for c, rep in product(NOT_SANITIZE_CHARS, REPLACE_TEXTS) ] + [ ["universal", "あ" + c + "い", rep, "あ" + c + "い"] for c, rep in product(NOT_SANITIZE_CHARS, REPLACE_TEXTS) ] + [ [pair.platform, "A" + pair.c + "B", pair.repl, "A" + pair.repl + "B"] for pair in AllPairs( OrderedDict( { "platform": [ "posix", "linux", "macos", Platform.POSIX, Platform.LINUX, Platform.MACOS, ], "c": INVALID_PATH_CHARS + unprintable_ascii_chars, "repl": REPLACE_TEXTS, } ) ) ] + [ [pair.platform, "A" + pair.c + "B", pair.repl, "A" + pair.c + "B"] for pair in AllPairs( OrderedDict( { "platform": [ "posix", "linux", "macos", Platform.POSIX, Platform.LINUX, Platform.MACOS, ], "c": [":", "*", "?", '"', "<", ">", "|"], "repl": REPLACE_TEXTS, } ) ) ], ) def test_normal_str(self, platform, value, replace_text, expected): sanitized_name = sanitize_filepath(value, platform=platform, replacement_text=replace_text) assert sanitized_name == expected assert isinstance(sanitized_name, str) validate_filepath(sanitized_name, platform=platform) assert is_valid_filepath(sanitized_name, platform=platform) @pytest.mark.parametrize( ["value", "test_platform", "expected"], [ [ f"abc/{reserved_keyword}/xyz", platform, f"abc/{reserved_keyword}_/xyz", ] for reserved_keyword, platform in product(WIN_RESERVED_FILE_NAMES, ["universal"]) ] + [ [ f"/abc/{reserved_keyword}/xyz", platform, f"/abc/{reserved_keyword}/xyz", ] for reserved_keyword, platform in product(WIN_RESERVED_FILE_NAMES, ["linux"]) ] + [ [ f"abc/{reserved_keyword}.txt", platform, f"abc/{reserved_keyword}_.txt", ] for reserved_keyword, platform in product(WIN_RESERVED_FILE_NAMES, ["universal"]) ] + [ [ f"/abc/{reserved_keyword}.txt", platform, f"/abc/{reserved_keyword}.txt", ] for reserved_keyword, platform in product(WIN_RESERVED_FILE_NAMES, ["linux"]) ] + [ [ f"C:\\abc\\{reserved_keyword}.txt", platform, f"C:\\abc\\{reserved_keyword}_.txt", ] for reserved_keyword, platform in product(WIN_RESERVED_FILE_NAMES, ["windows"]) ] + [ [f"{drive}\\{filename}", platform, f"{drive}\\{filename}_"] for drive, platform, filename in product( ["C:", "D:"], ["windows"], NTFS_RESERVED_FILE_NAMES ) ], ) def test_normal_reserved_name(self, value, test_platform, expected): filename = sanitize_filepath(value, platform=test_platform) assert filename == expected assert is_valid_filepath(filename, platform=test_platform) @pytest.mark.parametrize( ["value", "reserved_name_handler", "expected"], [ ["CON", ReservedNameHandler.add_trailing_underscore, "CON_"], ["CON", ReservedNameHandler.add_leading_underscore, "_CON"], ["CON", ReservedNameHandler.as_is, "CON"], ], ) def test_normal_reserved_name_handler(self, value, reserved_name_handler, expected): assert ( sanitize_filepath( value, platform="windows", reserved_name_handler=reserved_name_handler ) == expected ) def test_exception_reserved_name_handler(self): for platform in ["windows", "universal"]: with pytest.raises(ValidationError) as e: sanitize_filepath("CON", platform=platform, reserved_name_handler=raise_error) assert e.value.reason == ErrorReason.RESERVED_NAME @pytest.mark.parametrize( ["value", "check_reserved", "expected"], [ ["CON", True, "CON_"], ["CON", False, "CON"], ], ) def test_normal_check_reserved(self, value, check_reserved, expected): assert ( sanitize_filepath(value, platform="windows", check_reserved=check_reserved) == expected ) @pytest.mark.parametrize( ["value", "arn", "expected"], [ ["abc", ["abc"], "abc_"], ], ) def test_normal_additional_reserved_names(self, value, arn, expected): for platform in ["windows", "universal"]: assert ( sanitize_filepath( value, platform=platform, additional_reserved_names=arn, ) == expected ) @pytest.mark.parametrize( ["value", "replace_text", "expected"], [ [Path("AA" + c + "B"), rep, Path("AA" + rep + "B")] for c, rep in product(SANITIZE_CHARS, REPLACE_TEXTS) ] + [ [Path("A" + c + "B"), rep, Path("A" + c + "B")] for c, rep in product(NOT_SANITIZE_CHARS, REPLACE_TEXTS) ] + [ [Path("あ" + c + "い"), rep, Path("あ" + c + "い")] for c, rep in product(NOT_SANITIZE_CHARS, REPLACE_TEXTS) ], ) def test_normal_pathlike(self, value, replace_text, expected): sanitized_name = sanitize_filepath(value, replace_text) assert sanitized_name == expected assert isinstance(sanitized_name, Path) validate_filepath(sanitized_name) assert is_valid_filepath(sanitized_name) @pytest.mark.parametrize( ["test_platform", "value", "expected"], [ ["linux", "a/b/c.txt", "a/b/c.txt"], ["linux", "a//b?/c.txt", "a/b?/c.txt"], ["linux", "../a/./../b/c.txt", "../b/c.txt"], ["windows", "a/b/c.txt", "a\\b\\c.txt"], ["windows", "a//b?/c.txt", "a\\b\\c.txt"], ["windows", "../a/./../b/c.txt", "..\\b\\c.txt"], ["universal", "a/b/c.txt", "a/b/c.txt"], ["universal", "./", "."], ["universal", "./a/b/c.txt", "a/b/c.txt"], ["universal", "../a/./../b/c.txt", "../b/c.txt"], ["universal", "a//b?/c.txt", "a/b/c.txt"], ], ) def test_normal_relative_path(self, test_platform, value, expected): assert sanitize_filepath(value, platform=test_platform) == expected @pytest.mark.parametrize( ["test_platform", "value", "expected"], [ ["linux", "a/b/c.txt", "a/b/c.txt"], ["linux", "a//b?/c.txt", "a/b?/c.txt"], ["linux", "../a/./../b/c.txt", "../a/./../b/c.txt"], ["windows", "a/b/c.txt", "a\\b\\c.txt"], ["windows", "a//b?/c.txt", "a\\b\\c.txt"], ["windows", "../a/./../b/c.txt", "..\\a\\.\\..\\b\\c.txt"], ["universal", "a/b/c.txt", "a/b/c.txt"], ["universal", "./", "."], ["universal", "./a/b/c.txt", "./a/b/c.txt"], ["universal", "../a/./../b/c.txt", "../a/./../b/c.txt"], ["universal", "a//b?/c.txt", "a/b/c.txt"], ], ) def test_normal_not_normalize(self, test_platform, value, expected): assert sanitize_filepath(value, platform=test_platform, normalize=False) == expected @pytest.mark.parametrize( ["value"], [ [None], [""], ["?"], ], ) def test_normal_null_value_handler(self, value): assert ( sanitize_filepath(value, null_value_handler=NullValueHandler.return_null_string) == "" ) assert sanitize_filepath(value, null_value_handler=NullValueHandler.return_timestamp) != "" with pytest.raises(ValidationError): sanitize_filepath(value, null_value_handler=raise_error) @pytest.mark.parametrize( ["test_platform", "value", "replace_text", "expected"], [ ["linux", "/tmp/あいう\0えお.txt", "", "/tmp/あいうえお.txt"], ["linux", "/tmp/属\0性.txt", "-", "/tmp/属-性.txt"], ["universal", "tmp/あいう\0えお.txt", "", "tmp/あいうえお.txt"], ["universal", "tmp/属\0性.txt", "-", "tmp/属-性.txt"], ], ) def test_normal_multibyte(self, test_platform, value, replace_text, expected): sanitized_name = sanitize_filepath(value, replace_text, platform=test_platform) assert sanitized_name == expected validate_filepath(sanitized_name, platform=test_platform) assert is_valid_filepath(sanitized_name, platform=test_platform) @pytest.mark.parametrize( ["platform", "value", "expected"], [ ["windows", "a/b", "a\\b"], ["windows", "a\\b", "a\\b"], ["windows", "a\\\\b", "a\\b"], ["linux", "a/b", "a/b"], ["linux", "a//b", "a/b"], ["linux", "a\\b", "a/b"], ["linux", "a\\\\b", "a/b"], ["universal", "a/b", "a/b"], ["universal", "a//b", "a/b"], ["universal", "a\\b", "a/b"], ["universal", "a\\\\b", "a/b"], ], ) def test_normal_path_separator(self, platform, value, expected): sanitized = sanitize_filepath(value, platform=platform) assert sanitized == expected assert is_valid_filepath(sanitized, platform=platform) @pytest.mark.skipif(m_platform.system() != "Windows", reason="platform dependent tests") @pytest.mark.parametrize( ["value", "expected"], [ ["C:\\a\\b|c.txt", "C:\\a\\bc.txt"], ], ) def test_auto_platform_win(self, value, expected): if isinstance(expected, str): sanitized = sanitize_filepath(value, platform="auto") assert is_valid_filepath(sanitized, platform="auto") else: with pytest.raises(expected): sanitize_filepath(value, platform="auto") @pytest.mark.skipif(m_platform.system() != "Linux", reason="platform dependent tests") @pytest.mark.parametrize( ["value", "expected"], [ ["C:\\a\\b\\c.txt", ValidationError], ], ) def test_auto_platform_linux(self, value, expected): kwargs = { "platform": "auto", "validate_after_sanitize": True, } if isinstance(expected, str): sanitized = sanitize_filepath(value, **kwargs) assert is_valid_filepath(sanitized, platform="auto") return with pytest.raises(expected) as e: sanitize_filepath(value, **kwargs) assert e.value.reason == ErrorReason.MALFORMED_ABS_PATH @pytest.mark.parametrize( ["platform", "value"], [ ["windows", "CON \r"], ], ) def test_exception_invalid_after_sanitize(self, platform, value): print( "'{}'".format( sanitize_filepath(value, platform=platform, validate_after_sanitize=False) ), file=sys.stderr, ) with pytest.raises(ValidationError) as e: sanitize_filepath(value, platform=platform, validate_after_sanitize=True) assert e.value.reason == ErrorReason.INVALID_AFTER_SANITIZE @pytest.mark.parametrize( ["value", "expected"], [ [1, TypeError], [True, TypeError], [nan, TypeError], [inf, TypeError], ], ) def test_exception_type(self, value, expected): with pytest.raises(expected): sanitize_filepath(value) assert not is_valid_filepath(value) pathvalidate-3.2.0/test/test_handler.py000066400000000000000000000142301450146604500201740ustar00rootroot00000000000000import re import pytest from pathvalidate import ErrorReason, ValidationError from pathvalidate.handler import NullValueHandler, ReservedNameHandler, raise_error timstamp_regexp = re.compile(r"^\d+\.\d+$") class Test_raise_error: @pytest.mark.parametrize( ["exception"], [ [ ValidationError( description="hoge", reason=ErrorReason.INVALID_CHARACTER, ) ], [ ValidationError( description="foo", reason=ErrorReason.INVALID_AFTER_SANITIZE, ) ], ], ) def test_normal(self, exception): with pytest.raises(ValidationError) as e: raise_error(exception) assert exception == e.value class Test_NullValueHandler: @pytest.mark.parametrize( ["exception"], [ [ ValidationError( description="hoge", reason=ErrorReason.INVALID_CHARACTER, ) ], [ ValidationError( description="foo", reason=ErrorReason.INVALID_AFTER_SANITIZE, ) ], ], ) def test_return_null_string(self, exception): assert NullValueHandler.return_null_string(exception) == "" @pytest.mark.parametrize( ["exception"], [ [ ValidationError( description="hoge", reason=ErrorReason.INVALID_CHARACTER, ) ], [ ValidationError( description="foo", reason=ErrorReason.INVALID_AFTER_SANITIZE, ) ], ], ) def test_return_timestamp(self, exception): assert timstamp_regexp.search(NullValueHandler.return_timestamp(exception)) is not None class Test_ReservedNameHandler: @pytest.mark.parametrize( ["exception", "expected"], [ [ ValidationError( description="not reusable reserved name", reason=ErrorReason.RESERVED_NAME, reusable_name=False, reserved_name="hoge", ), "_hoge", ], [ ValidationError( description="do nothing to reusable reserved name", reason=ErrorReason.RESERVED_NAME, reusable_name=True, reserved_name="hoge", ), "hoge", ], [ ValidationError( description="do nothing to dot", reason=ErrorReason.RESERVED_NAME, reusable_name=False, reserved_name=".", ), ".", ], [ ValidationError( description="do nothing to double dot", reason=ErrorReason.RESERVED_NAME, reusable_name=False, reserved_name="..", ), "..", ], ], ) def test_add_leading_underscore(self, exception, expected): assert ReservedNameHandler.add_leading_underscore(exception) == expected @pytest.mark.parametrize( ["exception", "expected"], [ [ ValidationError( description="not reusable reserved name", reason=ErrorReason.RESERVED_NAME, reusable_name=False, reserved_name="hoge", ), "hoge_", ], [ ValidationError( description="do nothing to reusable reserved name", reason=ErrorReason.RESERVED_NAME, reusable_name=True, reserved_name="hoge", ), "hoge", ], [ ValidationError( description="do nothing to dot", reason=ErrorReason.RESERVED_NAME, reusable_name=False, reserved_name=".", ), ".", ], [ ValidationError( description="do nothing to double dot", reason=ErrorReason.RESERVED_NAME, reusable_name=False, reserved_name="..", ), "..", ], ], ) def test_add_trailing_underscore(self, exception, expected): assert ReservedNameHandler.add_trailing_underscore(exception) == expected @pytest.mark.parametrize( ["exception", "expected"], [ [ ValidationError( description="not reusable reserved name", reason=ErrorReason.RESERVED_NAME, reusable_name=False, reserved_name="hoge", ), "hoge", ], [ ValidationError( description="reusable reserved name", reason=ErrorReason.RESERVED_NAME, reusable_name=True, reserved_name="hoge", ), "hoge", ], [ ValidationError( description="dot", reason=ErrorReason.RESERVED_NAME, reusable_name=False, reserved_name=".", ), ".", ], [ ValidationError( description="double dot", reason=ErrorReason.RESERVED_NAME, reusable_name=False, reserved_name="..", ), "..", ], ], ) def test_as_is(self, exception, expected): assert ReservedNameHandler.as_is(exception) == expected pathvalidate-3.2.0/test/test_ltsv.py000066400000000000000000000047041450146604500175540ustar00rootroot00000000000000""" .. codeauthor:: Tsuyoshi Hombashi """ import itertools import pytest from pathvalidate import sanitize_ltsv_label, validate_ltsv_label from pathvalidate.error import ErrorReason, ValidationError from ._common import INVALID_WIN_FILENAME_CHARS, alphanum_chars VALID_LABEL_CHARS = alphanum_chars + ("_", ".", "-") INVALID_LABEL_CHARS = INVALID_WIN_FILENAME_CHARS + ( "!", "#", "$", "&", "'", "=", "~", "^", "@", "`", "[", "]", "+", ";", "{", "}", ",", "(", ")", "%", " ", "\t", "\n", "\r", "\f", "\v", ) class Test_validate_ltsv_label: VALID_CHARS = alphanum_chars INVALID_CHARS = INVALID_LABEL_CHARS @pytest.mark.parametrize( ["value"], [["abc" + valid_char + "hoge123"] for valid_char in VALID_CHARS] ) def test_normal(self, value): validate_ltsv_label(value) @pytest.mark.parametrize( ["value"], [["abc" + invalid_char + "hoge123"] for invalid_char in INVALID_CHARS] + [["あいうえお"], ["ラベル"]], ) def test_exception_invalid_char(self, value): with pytest.raises(ValidationError) as e: validate_ltsv_label(value) assert e.value.reason == ErrorReason.INVALID_CHARACTER class Test_sanitize_ltsv_label: TARGET_CHARS = INVALID_LABEL_CHARS NOT_TARGET_CHARS = alphanum_chars REPLACE_TEXT_LIST = ["", "_"] @pytest.mark.parametrize( ["value", "replace_text", "expected"], [ ["A" + c + "B", rep, "A" + rep + "B"] for c, rep in itertools.product(TARGET_CHARS, REPLACE_TEXT_LIST) ] + [ ["A" + c + "B", rep, "A" + c + "B"] for c, rep in itertools.product(NOT_TARGET_CHARS, REPLACE_TEXT_LIST) ], ) def test_normal(self, value, replace_text, expected): assert sanitize_ltsv_label(value, replace_text) == expected @pytest.mark.parametrize(["value", "expected"], [["aあいbうえcお", "abc"]]) def test_normal_multibyte(self, value, expected): sanitize_ltsv_label(value) @pytest.mark.parametrize( ["value", "expected"], [ ["", ValidationError], [None, ValidationError], [1, TypeError], [True, TypeError], ], ) def test_abnormal(self, value, expected): with pytest.raises(expected): sanitize_ltsv_label(value) pathvalidate-3.2.0/test/test_symbol.py000066400000000000000000000103661450146604500200720ustar00rootroot00000000000000""" .. codeauthor:: Tsuyoshi Hombashi """ import itertools import pytest from pathvalidate import ( ascii_symbols, replace_symbol, unprintable_ascii_chars, validate_symbol, validate_unprintable_char, ) from pathvalidate.error import ErrorReason, ValidationError from ._common import alphanum_chars class Test_validate_symbol: VALID_CHARS = alphanum_chars INVALID_CHARS = ascii_symbols @pytest.mark.parametrize( ["value"], [["abc" + valid_char + "hoge123"] for valid_char in VALID_CHARS] ) def test_normal(self, value): validate_symbol(value) @pytest.mark.parametrize(["value"], [["あいうえお"], ["シート"]]) def test_normal_multibyte(self, value): pytest.skip("TODO") validate_symbol(value) @pytest.mark.parametrize( ["value"], [ ["abc" + invalid_char + "hoge123"] for invalid_char in INVALID_CHARS + unprintable_ascii_chars ], ) def test_exception_invalid_char(self, value): with pytest.raises(ValidationError) as e: validate_symbol(value) assert e.value.reason == ErrorReason.INVALID_CHARACTER class Test_replace_symbol: TARGET_CHARS = ascii_symbols NOT_TARGET_CHARS = alphanum_chars REPLACE_TEXT_LIST = ["", "_"] @pytest.mark.parametrize( ["value", "replace_text", "expected"], [ ["A" + c + "B", rep, "A" + rep + "B"] for c, rep in itertools.product(TARGET_CHARS, REPLACE_TEXT_LIST) ] + [ ["A" + c + "B", rep, "A" + c + "B"] for c, rep in itertools.product(NOT_TARGET_CHARS, REPLACE_TEXT_LIST) ] + [["", "", ""]], ) def test_normal(self, value, replace_text, expected): assert replace_symbol(value, replace_text) == expected @pytest.mark.parametrize( ["value", "exclude_symbols", "expected"], [ ["/tmp/h!o|g$e.txt", ["/", "."], "/tmp/hoge.txt"], ["/tmp/h!o|g$e.txt", [], "tmphogetxt"], ["/tmp/h!o|g$e.txt", ["n", "o", "p"], "tmphogetxt"], ], ) def test_normal_exclude_symbols(self, value, exclude_symbols, expected): assert replace_symbol(value, exclude_symbols=exclude_symbols) == expected @pytest.mark.parametrize( ["value", "replace_text", "is_replace_consecutive_chars", "is_strip", "expected"], [ ["!a##b$$$c((((d]]]])", "_", True, True, "a_b_c_d"], ["!a##b$$$c((((d]]]])", "_", True, False, "_a_b_c_d_"], ["!a##b$$$c((((d]]]])", "_", False, True, "a__b___c____d"], ["!a##b$$$c((((d]]]])", "_", False, False, "_a__b___c____d_____"], ], ) def test_normal_consecutive( self, value, replace_text, is_replace_consecutive_chars, is_strip, expected ): assert ( replace_symbol( value, replace_text, is_replace_consecutive_chars=is_replace_consecutive_chars, is_strip=is_strip, ) == expected ) @pytest.mark.parametrize( ["value", "expected"], [ [None, TypeError], [1, TypeError], [True, TypeError], ], ) def test_abnormal(self, value, expected): with pytest.raises(expected): replace_symbol(value) class Test_validate_unprintable_char: VALID_CHARS = alphanum_chars INVALID_CHARS = unprintable_ascii_chars @pytest.mark.parametrize( ["value"], [["abc" + valid_char + "hoge123"] for valid_char in VALID_CHARS] ) def test_normal(self, value): validate_unprintable_char(value) @pytest.mark.parametrize(["value"], [["あいうえお"], ["シート"]]) def test_normal_multibyte(self, value): pytest.skip("TODO") validate_unprintable_char(value) @pytest.mark.parametrize( ["value"], [ ["abc" + invalid_char + "hoge123"] for invalid_char in INVALID_CHARS + unprintable_ascii_chars ], ) def test_exception_invalid_char(self, value): with pytest.raises(ValidationError) as e: validate_unprintable_char(value) assert e.value.reason == ErrorReason.INVALID_CHARACTER pathvalidate-3.2.0/tox.ini000066400000000000000000000030741450146604500155060ustar00rootroot00000000000000[tox] envlist = py{37,38,39,310,311} pypy3 build cov docs fmt lint readme skip_missing_interpreters = true [testenv] passenv = * extras = test allowlist_externals = pytest commands = pytest {posargs} [testenv:build] deps = build>=0.10 twine wheel commands = python -m build twine check dist/*.whl dist/*.tar.gz [testenv:clean] skip_install = true deps = cleanpy>=0.4 commands = cleanpy --all --exclude-envs . [testenv:cov] extras = test deps = coverage[toml]>=5 commands = coverage run -m pytest {posargs:-vv} coverage report -m [testenv:docs] extras = docs commands = sphinx-build docs/ docs/_build [testenv:fmt] skip_install = true deps = autoflake>=2 black>=23.1 isort>=5 commands = autoflake --in-place --recursive --remove-all-unused-imports --ignore-init-module-imports --exclude ".pytype" . isort . black setup.py test pathvalidate [testenv:lint] skip_install = true deps = codespell>=2 mypy>=1 pylama>=8.4.1 types-click commands = mypy pathvalidate setup.py codespell pathvalidate docs/pages examples test -q2 --check-filenames pylama pathvalidate test setup.py [testenv:lint-examples] skip_install = true changedir = examples deps = jupyter mypy>=1 pathvalidate>=3 types-click commands = jupyter nbconvert pathvalidate_examples.ipynb --to python mypy pathvalidate_examples.py --strict [testenv:readme] skip_install = true changedir = docs deps = readmemaker>=1.1.0 commands = python make_readme.py