pax_global_header00006660000000000000000000000064141240751340014513gustar00rootroot0000000000000052 comment=00076c0df36b004ce3cf4bc350156fd939f249c1 pathvalidate-2.5.0/000077500000000000000000000000001412407513400141655ustar00rootroot00000000000000pathvalidate-2.5.0/.github/000077500000000000000000000000001412407513400155255ustar00rootroot00000000000000pathvalidate-2.5.0/.github/FUNDING.yml000066400000000000000000000011421412407513400173400ustar00rootroot00000000000000# 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-2.5.0/.github/workflows/000077500000000000000000000000001412407513400175625ustar00rootroot00000000000000pathvalidate-2.5.0/.github/workflows/codeql-analysis.yml000066400000000000000000000013071412407513400233760ustar00rootroot00000000000000name: "CodeQL" on: push: branches: [ master ] pull_request: # The branches below must be a subset of the branches above branches: [ master ] schedule: - cron: '24 8 * * 6' jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: [ 'python' ] steps: - name: Checkout repository uses: actions/checkout@v2 - name: Initialize CodeQL uses: github/codeql-action/init@v1 with: languages: ${{ matrix.language }} - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v1 pathvalidate-2.5.0/.github/workflows/tests.yml000066400000000000000000000032171412407513400214520ustar00rootroot00000000000000name: Tests on: [push, pull_request] jobs: unit-test: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: python-version: [3.6, 3.7, 3.8, 3.9, 3.10.0-rc.2, pypy3] os: [ubuntu-latest, macos-latest, windows-latest] exclude: - os: windows-latest python-version: 3.6 timeout-minutes: 20 steps: - uses: actions/checkout@v2 - name: Setup Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install pip run: python -m pip install --upgrade --disable-pip-version-check "pip>=21.1" - name: Get pip cache dir id: pip-cache run: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip uses: actions/cache@v2 with: path: | ${{ steps.pip-cache.outputs.dir }} ./.tox key: ${{ matrix.os }}-${{ matrix.python-version }}-pip-${{ hashFiles('**/requirements.txt') }} restore-keys: | ${{ matrix.os }}-${{ matrix.python-version }}-pip- - name: Install dependencies run: make setup-ci - name: Run tox run: tox -e cov -- --discord-verbose=0 env: PYTEST_DISCORD_WEBHOOK: ${{ secrets.PYTEST_DISCORD_WEBHOOK }} - name: Upload coverage report run: | python -m pip install --upgrade --disable-pip-version-check coveralls coveralls --service=github env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.8' pathvalidate-2.5.0/.gitignore000066400000000000000000000026001412407513400161530ustar00rootroot00000000000000# 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-2.5.0/LICENSE000066400000000000000000000020741412407513400151750ustar00rootroot00000000000000The 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-2.5.0/MANIFEST.in000066400000000000000000000003621412407513400157240ustar00rootroot00000000000000include 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-2.5.0/Makefile000066400000000000000000000022301412407513400156220ustar00rootroot00000000000000AUTHOR := 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 .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 @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-2.5.0/README.rst000066400000000000000000000213501412407513400156550ustar00rootroot00000000000000.. 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/codeql-analysis.yml/badge.svg :target: https://github.com/thombashi/pathvalidate/actions/workflows/codeql-analysis.yml :alt: CodeQL Features --------- - Sanitize/Validate a string as a: - file name - file path - file name/path argument validator/sanitizer for ``argparse`` and ``click`` - Multi platform support: - sanitize/validate file names/paths for a specific platform (``Linux``/``Windows``/``macOS``/``Posix``) or ``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|.t', '|', '<'), value='fi:l*e/p"a?t>h|.th|.th|.t`__ pathvalidate-2.5.0/docs/000077500000000000000000000000001412407513400151155ustar00rootroot00000000000000pathvalidate-2.5.0/docs/Makefile000066400000000000000000000176301412407513400165640ustar00rootroot00000000000000# 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-2.5.0/docs/conf.py000066400000000000000000000235211412407513400164170ustar00rootroot00000000000000import 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-2.5.0/docs/index.rst000066400000000000000000000010721412407513400167560ustar00rootroot00000000000000Welcome 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-2.5.0/docs/make.bat000066400000000000000000000164471412407513400165360ustar00rootroot00000000000000@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-2.5.0/docs/make_readme.py000066400000000000000000000045371412407513400177320ustar00rootroot00000000000000#!/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-2.5.0/docs/pages/000077500000000000000000000000001412407513400162145ustar00rootroot00000000000000pathvalidate-2.5.0/docs/pages/examples/000077500000000000000000000000001412407513400200325ustar00rootroot00000000000000pathvalidate-2.5.0/docs/pages/examples/argparse.rst000066400000000000000000000004151412407513400223700ustar00rootroot00000000000000filename/filepath validator for argparse -------------------------------------------------------- .. include:: argparse_validator.txt filename/filepath sanitizer for argparse -------------------------------------------------------- .. include:: argparse_sanitizer.txt pathvalidate-2.5.0/docs/pages/examples/argparse_sanitizer.txt000066400000000000000000000013601412407513400244670ustar00rootroot00000000000000: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-2.5.0/docs/pages/examples/argparse_validator.txt000066400000000000000000000021101412407513400244360ustar00rootroot00000000000000:Sample Code: .. code-block:: python from argparse import ArgumentParser from pathvalidate.argparse import validate_filename_arg, validate_filepath_arg parser = ArgumentParser() parser.add_argument("--filepath", type=validate_filepath_arg) parser.add_argument("--filename", type=validate_filename_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_validate.py --filename eg filename: eg $ ./examples/argparse_validate.py --filepath e?g usage: argparse_validate.py [-h] [--filepath FILEPATH] [--filename FILENAME] argparse_validate.py: error: argument --filepath: invalid char found: invalids=('?'), value='e?g', reason=INVALID_CHARACTER, target-platform=Windows .. note:: ``validate_filepath_arg`` consider ``platform`` as of ``"auto"`` if the input is an absolute file path. pathvalidate-2.5.0/docs/pages/examples/click.rst000066400000000000000000000004021412407513400216450ustar00rootroot00000000000000filename/filepath validator for click -------------------------------------------------------- .. include:: click_validator.txt filename/filepath sanitizer for click -------------------------------------------------------- .. include:: click_sanitizer.txt pathvalidate-2.5.0/docs/pages/examples/click_sanitizer.txt000066400000000000000000000012541412407513400237520ustar00rootroot00000000000000: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("filename: {}".format(filename)) if filepath: click.echo("filepath: {}".format(filepath)) if __name__ == "__main__": cli() :Output: .. code-block:: none $ ./examples/click_sanitize.py --filename a/b filename: ab pathvalidate-2.5.0/docs/pages/examples/click_validator.txt000066400000000000000000000016371412407513400237340ustar00rootroot00000000000000: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, filepath): if filename: click.echo("filename: {}".format(filename)) if filepath: click.echo("filepath: {}".format(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] Error: Invalid value for "--filepath": invalid char found: invalids=('?'), value='e?g', reason=INVALID_CHARACTER, target-platform=Windows pathvalidate-2.5.0/docs/pages/examples/index.rst000066400000000000000000000001521412407513400216710ustar00rootroot00000000000000Examples ======== .. toctree:: :maxdepth: 3 sanitize validate is_valid argparse click pathvalidate-2.5.0/docs/pages/examples/is_valid.rst000066400000000000000000000007031412407513400223560ustar00rootroot00000000000000.. _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-2.5.0/docs/pages/examples/is_valid_filename_code.txt000066400000000000000000000010411412407513400252130ustar00rootroot00000000000000: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-2.5.0/docs/pages/examples/sanitize_filepath_code.txt000066400000000000000000000006671412407513400253000ustar00rootroot00000000000000: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-2.5.0/docs/pages/examples/sanitize_replace_symbol_code.txt000066400000000000000000000004221412407513400264710ustar00rootroot00000000000000: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-2.5.0/docs/pages/examples/sanitize_var_name_code.txt000066400000000000000000000003201412407513400252560ustar00rootroot00000000000000: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-2.5.0/docs/pages/examples/validate.rst000066400000000000000000000007611412407513400223610ustar00rootroot00000000000000.. _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-2.5.0/docs/pages/examples/validate_filename_code.txt000066400000000000000000000012651412407513400252220ustar00rootroot00000000000000:Sample Code: .. code-block:: python import sys from pathvalidate import ValidationError, validate_filename try: validate_filename("fi:l*e/p\"a?t>h|.t', '|', '<'), value='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-2.5.0/docs/pages/genindex.rst000066400000000000000000000000701412407513400205440ustar00rootroot00000000000000Indices and tables ================== * :ref:`genindex`pathvalidate-2.5.0/docs/pages/introduction/000077500000000000000000000000001412407513400207355ustar00rootroot00000000000000pathvalidate-2.5.0/docs/pages/introduction/badges.txt000066400000000000000000000023341412407513400227250ustar00rootroot00000000000000.. 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/codeql-analysis.yml/badge.svg :target: https://github.com/thombashi/pathvalidate/actions/workflows/codeql-analysis.yml :alt: CodeQL pathvalidate-2.5.0/docs/pages/introduction/feature.txt000066400000000000000000000005601412407513400231320ustar00rootroot00000000000000Features --------- - Sanitize/Validate a string as a: - file name - file path - file name/path argument validator/sanitizer for ``argparse`` and ``click`` - Multi platform support: - sanitize/validate file names/paths for a specific platform (``Linux``/``Windows``/``macOS``/``Posix``) or ``universal`` (platform independent) - Multibyte character support pathvalidate-2.5.0/docs/pages/introduction/index.rst000066400000000000000000000006271412407513400226030ustar00rootroot00000000000000pathvalidate ============= .. include:: badges.txt Summary ------- .. include:: summary.txt .. raw:: html

.. include:: feature.txt .. include:: installation.rst pathvalidate-2.5.0/docs/pages/introduction/installation.rst000066400000000000000000000006641412407513400241760ustar00rootroot00000000000000Installation ============ 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.6+ No external dependencies. pathvalidate-2.5.0/docs/pages/introduction/summary.txt000066400000000000000000000001411412407513400231670ustar00rootroot00000000000000pathvalidate is a Python library to sanitize/validate a string such as filenames/file-paths/etc. pathvalidate-2.5.0/docs/pages/links.rst000066400000000000000000000005501412407513400200660ustar00rootroot00000000000000Changelog ========== 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-2.5.0/docs/pages/reference/000077500000000000000000000000001412407513400201525ustar00rootroot00000000000000pathvalidate-2.5.0/docs/pages/reference/error.rst000066400000000000000000000003211412407513400220310ustar00rootroot00000000000000Errors --------------- .. autoexception:: pathvalidate.error.ErrorReason :members: :show-inheritance: .. autoexception:: pathvalidate.error.ValidationError :undoc-members: :show-inheritance: pathvalidate-2.5.0/docs/pages/reference/function.rst000066400000000000000000000013421412407513400225310ustar00rootroot00000000000000Functions --------------- File name validation/sanitization ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. autofunction:: pathvalidate.validate_filename .. autofunction:: pathvalidate.sanitize_filename Check a file name ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. autofunction:: pathvalidate.is_valid_filename File path validate/sanitize ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. autofunction:: pathvalidate.validate_filepath .. autofunction:: pathvalidate.sanitize_filepath Check a file path ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. autofunction:: pathvalidate.is_valid_filepath Symbol validate/sanitize ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. autofunction:: pathvalidate.validate_symbol .. autofunction:: pathvalidate.replace_symbol pathvalidate-2.5.0/docs/pages/reference/index.rst000066400000000000000000000001131412407513400220060ustar00rootroot00000000000000Reference ========= .. toctree:: :maxdepth: 3 error function pathvalidate-2.5.0/docs/pages/reference/platform.txt000066400000000000000000000004611412407513400225400ustar00rootroot00000000000000Valid specifiers are 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-2.5.0/docs/pages/sponsors.rst000066400000000000000000000004541412407513400206370ustar00rootroot00000000000000Sponsors ==================================== .. image:: https://avatars0.githubusercontent.com/u/44389260?s=48&u=6da7176e51ae2654bcfd22564772ef8a3bb22318&v=4 :target: https://github.com/chasbecker :alt: Charles Becker (chasbecker) `Become a sponsor `__ pathvalidate-2.5.0/examples/000077500000000000000000000000001412407513400160035ustar00rootroot00000000000000pathvalidate-2.5.0/examples/README.rst000066400000000000000000000001401412407513400174650ustar00rootroot00000000000000https://nbviewer.jupyter.org/github/thombashi/pathvalidate/tree/master/ipynb/pathvalidate.ipynb pathvalidate-2.5.0/examples/argparse_sanitize.py000077500000000000000000000007041412407513400220730ustar00rootroot00000000000000#!/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-2.5.0/examples/argparse_validate.py000077500000000000000000000007041412407513400220360ustar00rootroot00000000000000#!/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-2.5.0/examples/click_sanitize.py000077500000000000000000000006731412407513400213610ustar00rootroot00000000000000#!/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, filepath): if filename: click.echo(f"filename: {filename}") if filepath: click.echo(f"filepath: {filepath}") if __name__ == "__main__": cli() pathvalidate-2.5.0/examples/click_validate.py000077500000000000000000000006731412407513400213240ustar00rootroot00000000000000#!/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, filepath): if filename: click.echo(f"filename: {filename}") if filepath: click.echo(f"filepath: {filepath}") if __name__ == "__main__": cli() pathvalidate-2.5.0/examples/pathvalidate.ipynb000066400000000000000000000134051412407513400215170ustar00rootroot00000000000000{ "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": [ "invalid char 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 """ from .__version__ import __author__, __copyright__, __email__, __license__, __version__ from ._base import AbstractSanitizer, AbstractValidator from ._common import ( Platform, ascii_symbols, normalize_platform, replace_ansi_escape, replace_unprintable_char, unprintable_ascii_chars, validate_null_string, validate_pathtype, ) from ._filename import FileNameSanitizer, is_valid_filename, sanitize_filename, validate_filename from ._filepath import ( FilePathSanitizer, is_valid_filepath, sanitize_file_path, sanitize_filepath, validate_file_path, validate_filepath, ) from ._ltsv import sanitize_ltsv_label, validate_ltsv_label from ._symbol import replace_symbol, validate_symbol from .error import ( ErrorReason, InvalidCharError, InvalidLengthError, InvalidReservedNameError, NullNameError, ReservedNameError, ValidationError, ValidReservedNameError, ) pathvalidate-2.5.0/pathvalidate/__version__.py000066400000000000000000000003111412407513400214610ustar00rootroot00000000000000__author__ = "Tsuyoshi Hombashi" __copyright__ = f"Copyright 2016, {__author__}" __license__ = "MIT License" __version__ = "2.5.0" __maintainer__ = __author__ __email__ = "tsuyoshi.hombashi@gmail.com" pathvalidate-2.5.0/pathvalidate/_base.py000066400000000000000000000101641412407513400202600ustar00rootroot00000000000000""" .. codeauthor:: Tsuyoshi Hombashi """ import abc import os from typing import Optional, Tuple, cast from ._common import PathType, Platform, PlatformType, normalize_platform, unprintable_ascii_chars from .error import ReservedNameError, ValidationError from .handler import Handler, return_null_string DEFAULT_MIN_LEN = 1 class BaseFile: _INVALID_PATH_CHARS = "".join(unprintable_ascii_chars) _INVALID_FILENAME_CHARS = _INVALID_PATH_CHARS + "/" _INVALID_WIN_PATH_CHARS = _INVALID_PATH_CHARS + ':*?"<>|\t\n\r\x0b\x0c' _INVALID_WIN_FILENAME_CHARS = _INVALID_FILENAME_CHARS + _INVALID_WIN_PATH_CHARS + "\\" _ERROR_MSG_TEMPLATE = "invalid char found: invalids=({invalid}), value={value}" @property def platform(self) -> Platform: return self.__platform @property def reserved_keywords(self) -> Tuple[str, ...]: return tuple() @property def min_len(self) -> int: return self._min_len @property def max_len(self) -> int: return self._max_len def __init__( self, min_len: int, max_len: int, check_reserved: bool, null_value_handler: Optional[Handler] = None, platform_max_len: Optional[int] = None, platform: PlatformType = None, ) -> None: self.__platform = normalize_platform(platform) self._check_reserved = check_reserved if null_value_handler is None: null_value_handler = return_null_string self._null_value_handler = null_value_handler if min_len <= 0: min_len = DEFAULT_MIN_LEN self._min_len = max(min_len, 1) 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 = cast(int, max_len) self._max_len = min(self._max_len, platform_max_len) self._validate_max_len() def _is_posix(self) -> bool: return self.platform == Platform.POSIX def _is_universal(self) -> bool: return self.platform == Platform.UNIVERSAL def _is_linux(self) -> bool: return self.platform == Platform.LINUX def _is_windows(self) -> bool: return self.platform == Platform.WINDOWS def _is_macos(self) -> bool: return self.platform == Platform.MACOS def _validate_max_len(self) -> None: if self.max_len < 1: raise ValueError("max_len must be greater or equals to one") if self.min_len > self.max_len: raise ValueError("min_len must be lower than max_len") 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): @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): @abc.abstractmethod def sanitize(self, value: PathType, replacement_text: str = "") -> PathType: # pragma: no cover pass class BaseValidator(AbstractValidator): def _validate_reserved_keywords(self, name: str) -> None: if not self._check_reserved: return root_name = self.__extract_root_name(name) if self._is_reserved_keyword(root_name.upper()): raise ReservedNameError( f"'{root_name}' is a reserved name", reusable_name=False, reserved_name=root_name, platform=self.platform, ) @staticmethod def __extract_root_name(path: str) -> str: return os.path.splitext(os.path.basename(path))[0] pathvalidate-2.5.0/pathvalidate/_common.py000066400000000000000000000070771412407513400206470ustar00rootroot00000000000000""" .. codeauthor:: Tsuyoshi Hombashi """ import enum import platform import re import string from pathlib import Path from typing import Any, List, Optional, Union, cast _re_whitespaces = re.compile(r"^[\s]+$") @enum.unique class Platform(enum.Enum): POSIX = "POSIX" UNIVERSAL = "universal" LINUX = "Linux" WINDOWS = "Windows" MACOS = "macOS" PathType = Union[str, Path] PlatformType = Union[str, Platform, None] def is_pathlike_obj(value: PathType) -> bool: return isinstance(value, Path) 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 is_pathlike_obj(text): return if allow_whitespaces and _re_whitespaces.search(str(text)): return if is_null_string(text): if not error_msg: error_msg = "the value must be a not empty" raise ValidationError( description=error_msg, reason=ErrorReason.NULL_NAME, ) raise TypeError(f"text must be a string: actual={type(text)}") def validate_null_string(text: PathType, error_msg: Optional[str] = None) -> None: # Deprecated: alias to validate_pathtype validate_pathtype(text, False, error_msg) def preprocess(name: PathType) -> str: if is_pathlike_obj(name): name = str(name) return cast(str, 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 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: PlatformType) -> Platform: if isinstance(name, Platform): return name if name: 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-2.5.0/pathvalidate/_const.py000066400000000000000000000004121412407513400204670ustar00rootroot00000000000000_NTFS_RESERVED_FILE_NAMES = ( "$Mft", "$MftMirr", "$LogFile", "$Volume", "$AttrDef", "$Bitmap", "$Boot", "$BadClus", "$Secure", "$Upcase", "$Extend", "$Quota", "$ObjId", "$Reparse", ) # Only in root directory pathvalidate-2.5.0/pathvalidate/_filename.py000066400000000000000000000271441412407513400211340ustar00rootroot00000000000000""" .. codeauthor:: Tsuyoshi Hombashi """ import itertools import ntpath import posixpath import re from pathlib import Path from typing import Optional, Pattern, Tuple from ._base import DEFAULT_MIN_LEN, AbstractSanitizer, BaseFile, BaseValidator from ._common import ( PathType, Platform, PlatformType, findall_to_str, is_pathlike_obj, preprocess, validate_pathtype, ) from .error import ErrorReason, InvalidCharError, InvalidLengthError, ValidationError from .handler import Handler _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, min_len: int = DEFAULT_MIN_LEN, max_len: int = _DEFAULT_MAX_FILENAME_LEN, platform: PlatformType = None, check_reserved: bool = True, null_value_handler: Optional[Handler] = None, ) -> None: super().__init__( min_len=min_len, max_len=max_len, check_reserved=check_reserved, null_value_handler=null_value_handler, platform_max_len=_DEFAULT_MAX_FILENAME_LEN, platform=platform, ) self._sanitize_regexp = self._get_sanitize_regexp() self.__validator = FileNameValidator( min_len=self.min_len, max_len=self.max_len, check_reserved=check_reserved, platform=self.platform, ) def sanitize(self, value: PathType, replacement_text: str = "") -> PathType: try: validate_pathtype(value, allow_whitespaces=False if self._is_windows() else True) except ValidationError as e: if e.reason == ErrorReason.NULL_NAME: 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 and e.reusable_name is False: sanitized_filename = re.sub( re.escape(e.reserved_name), f"{e.reserved_name}_", sanitized_filename ) elif e.reason == ErrorReason.INVALID_CHARACTER: if self.platform in [Platform.UNIVERSAL, Platform.WINDOWS]: # Do not end a file or directory name with a space or a period sanitized_filename = sanitized_filename.rstrip(" .") elif e.reason == ErrorReason.NULL_NAME: return self._null_value_handler(e) else: raise if is_pathlike_obj(value): return Path(sanitized_filename) return sanitized_filename def _get_sanitize_regexp(self) -> Pattern: if self.platform in [Platform.UNIVERSAL, Platform.WINDOWS]: 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(): return ( common_keywords + self._WINDOWS_RESERVED_FILE_NAMES + self._MACOS_RESERVED_FILE_NAMES ) if self._is_windows(): return common_keywords + self._WINDOWS_RESERVED_FILE_NAMES if self._is_posix() or self._is_macos(): return common_keywords + self._MACOS_RESERVED_FILE_NAMES return common_keywords def __init__( self, min_len: int = DEFAULT_MIN_LEN, max_len: int = _DEFAULT_MAX_FILENAME_LEN, platform: PlatformType = None, check_reserved: bool = True, ) -> None: super().__init__( min_len=min_len, max_len=max_len, check_reserved=check_reserved, platform_max_len=_DEFAULT_MAX_FILENAME_LEN, platform=platform, ) def validate(self, value: PathType) -> None: validate_pathtype( value, allow_whitespaces=False if self.platform in [Platform.UNIVERSAL, Platform.WINDOWS] else True, ) unicode_filename = preprocess(value) value_len = len(unicode_filename) self.validate_abspath(unicode_filename) if value_len > self.max_len: raise InvalidLengthError( f"filename is too long: expected<={self.max_len:d}, actual={value_len:d}" ) if value_len < self.min_len: raise InvalidLengthError( f"filename is too short: expected>={self.min_len:d}, actual={value_len:d}" ) self._validate_reserved_keywords(unicode_filename) if self._is_universal() or self._is_windows(): self.__validate_win_filename(unicode_filename) else: self.__validate_unix_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_universal() or self._is_windows(): if ntpath.isabs(value): raise err if posixpath.isabs(value): raise err def __validate_unix_filename(self, unicode_filename: str) -> None: match = _RE_INVALID_FILENAME.findall(unicode_filename) if match: raise InvalidCharError( self._ERROR_MSG_TEMPLATE.format( invalid=findall_to_str(match), value=repr(unicode_filename) ) ) def __validate_win_filename(self, unicode_filename: str) -> None: match = _RE_INVALID_WIN_FILENAME.findall(unicode_filename) if match: raise InvalidCharError( self._ERROR_MSG_TEMPLATE.format( invalid=findall_to_str(match), value=repr(unicode_filename) ), platform=Platform.WINDOWS, ) if unicode_filename in (".", ".."): return if unicode_filename[-1] in (" ", "."): raise InvalidCharError( self._ERROR_MSG_TEMPLATE.format( invalid=re.escape(unicode_filename[-1]), value=repr(unicode_filename) ), platform=Platform.WINDOWS, description="Do not end a file or directory name with a space or a period", ) def validate_filename( filename: PathType, platform: Optional[str] = None, min_len: int = DEFAULT_MIN_LEN, max_len: int = _DEFAULT_MAX_FILENAME_LEN, check_reserved: bool = True, ) -> 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 length of the ``filename``. The value must be greater or equal to one. Defaults to ``1``. max_len: Maximum length of the ``filename``. The value must be lower than: - ``Linux``: 4096 - ``macOS``: 1024 - ``Windows``: 260 - ``universal``: 260 Defaults to ``255``. check_reserved: If |True|, check reserved names of the ``platform``. 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 platform: |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, check_reserved=check_reserved ).validate(filename) def is_valid_filename( filename: PathType, platform: Optional[str] = None, min_len: int = DEFAULT_MIN_LEN, max_len: Optional[int] = None, check_reserved: bool = True, ) -> bool: """Check whether the ``filename`` is a valid name or not. Args: filename: A filename to be checked. 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, check_reserved=check_reserved, ).is_valid(filename) def sanitize_filename( filename: PathType, replacement_text: str = "", platform: Optional[str] = None, max_len: Optional[int] = _DEFAULT_MAX_FILENAME_LEN, check_reserved: bool = True, null_value_handler: Optional[Handler] = None, ) -> PathType: """Make a valid filename from a string. To make a valid filename the function does: - 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| - Append underscore (``"_"``) at the tail of the name if sanitized name is one of the reserved names by operating systems (only when ``check_reserved`` is |True|). 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 length of the ``filename`` length. Truncate the name length if the ``filename`` length exceeds this value. Defaults to ``255``. check_reserved: If |True|, sanitize reserved names of the ``platform``. null_value_handler: Function called when a value after sanitization is an empty string. Defaults to ``pathvalidate.handler.return_null_string()`` that just return ``""``. 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` """ return FileNameSanitizer( platform=platform, max_len=-1 if max_len is None else max_len, check_reserved=check_reserved, null_value_handler=null_value_handler, ).sanitize(filename, replacement_text) pathvalidate-2.5.0/pathvalidate/_filepath.py000066400000000000000000000350411412407513400211430ustar00rootroot00000000000000""" .. codeauthor:: Tsuyoshi Hombashi """ import ntpath import os.path import posixpath import re from pathlib import Path from typing import List, Optional, Pattern, Tuple from ._base import DEFAULT_MIN_LEN, AbstractSanitizer, BaseFile, BaseValidator from ._common import ( PathType, Platform, PlatformType, findall_to_str, is_pathlike_obj, preprocess, validate_pathtype, ) from ._const import _NTFS_RESERVED_FILE_NAMES from ._filename import FileNameSanitizer, FileNameValidator from .error import ( ErrorReason, InvalidCharError, InvalidLengthError, ReservedNameError, ValidationError, ) from .handler import Handler _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, min_len: int = DEFAULT_MIN_LEN, max_len: int = -1, platform: PlatformType = None, check_reserved: bool = True, null_value_handler: Optional[Handler] = None, normalize: bool = True, ) -> None: super().__init__( min_len=min_len, max_len=max_len, check_reserved=check_reserved, null_value_handler=null_value_handler, platform=platform, ) self._sanitize_regexp = self._get_sanitize_regexp() self.__fpath_validator = FilePathValidator( min_len=self.min_len, max_len=self.max_len, check_reserved=check_reserved, platform=self.platform, ) self.__fname_sanitizer = FileNameSanitizer( min_len=self.min_len, max_len=self.max_len, check_reserved=check_reserved, platform=self.platform, ) self.__normalize = normalize if self._is_universal() or self._is_windows(): 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=False if self._is_windows() else True) except ValidationError as e: if e.reason == ErrorReason.NULL_NAME: return self._null_value_handler(e) raise self.__fpath_validator.validate_abspath(value) unicode_filepath = preprocess(value) if self.__normalize: unicode_filepath = os.path.normpath(unicode_filepath) drive, unicode_filepath = self.__split_drive(unicode_filepath) sanitized_path = self._sanitize_regexp.sub(replacement_text, unicode_filepath) if self._is_windows(): path_separator = "\\" else: path_separator = "/" 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)) if not sanitized_entry: if not sanitized_entries: sanitized_entries.append("") continue sanitized_entries.append(sanitized_entry) sanitized_path = path_separator.join(sanitized_entries) try: self.__fpath_validator.validate(sanitized_path) except ValidationError as e: if e.reason == ErrorReason.NULL_NAME: return self._null_value_handler(e) raise if is_pathlike_obj(value): return Path(sanitized_path) return sanitized_path def _get_sanitize_regexp(self) -> Pattern: if self.platform in [Platform.UNIVERSAL, Platform.WINDOWS]: return _RE_INVALID_WIN_PATH return _RE_INVALID_PATH 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, platform: PlatformType = None, check_reserved: bool = True, ) -> None: super().__init__( min_len=min_len, max_len=max_len, check_reserved=check_reserved, platform=platform, ) self.__fname_validator = FileNameValidator( min_len=min_len, max_len=max_len, check_reserved=check_reserved, platform=platform ) if self._is_universal() or self._is_windows(): self.__split_drive = ntpath.splitdrive else: self.__split_drive = posixpath.splitdrive def validate(self, value: PathType) -> None: validate_pathtype( value, allow_whitespaces=False if self.platform in [Platform.UNIVERSAL, Platform.WINDOWS] else True, ) self.validate_abspath(value) _drive, value = self.__split_drive(str(value)) if not value: return filepath = os.path.normpath(value) unicode_filepath = preprocess(filepath) value_len = len(unicode_filepath) if value_len > self.max_len: raise InvalidLengthError( f"file path is too long: expected<={self.max_len:d}, actual={value_len:d}" ) if value_len < self.min_len: raise InvalidLengthError( "file path is too short: expected>={:d}, actual={:d}".format( self.min_len, value_len ) ) 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_universal() or self._is_windows(): self.__validate_win_filepath(unicode_filepath) else: self.__validate_unix_filepath(unicode_filepath) def validate_abspath(self, value: PathType) -> None: value = str(value) 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 correspond" + " with 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=( "{}. expected a platform independent file path".format( "POSIX absolute file path found" if is_posix_abs else "NT absolute file path found" ) ), platform=self.platform, reason=ErrorReason.MALFORMED_ABS_PATH, ) if any([self._is_windows(), self._is_universal()]) 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( self._ERROR_MSG_TEMPLATE.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( self._ERROR_MSG_TEMPLATE.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[str] = None, min_len: int = DEFAULT_MIN_LEN, max_len: Optional[int] = None, check_reserved: bool = True, ) -> None: """Verifying whether the ``file_path`` is a valid file path or not. Args: file_path: File path to validate. platform: Target platform name of the file path. .. include:: platform.txt min_len: Minimum length of the ``file_path``. The value must be greater or equal to one. Defaults to ``1``. max_len: Maximum length of the ``file_path`` length. If the value is |None| or minus, automatically determined by the ``platform``: - ``Linux``: 4096 - ``macOS``: 1024 - ``Windows``: 260 - ``universal``: 260 check_reserved: If |True|, check reserved names of the ``platform``. 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 platform: |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, check_reserved=check_reserved, ).validate(file_path) def validate_file_path(file_path, platform=None, max_path_len=None): # Deprecated validate_filepath(file_path, platform, max_path_len) def is_valid_filepath( file_path: PathType, platform: Optional[str] = None, min_len: int = DEFAULT_MIN_LEN, max_len: Optional[int] = None, check_reserved: bool = True, ) -> bool: """Check whether the ``file_path`` is a valid name or not. Args: file_path: A filepath to be checked. 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, check_reserved=check_reserved, ).is_valid(file_path) def sanitize_filepath( file_path: PathType, replacement_text: str = "", platform: Optional[str] = None, max_len: Optional[int] = None, check_reserved: bool = True, null_value_handler: Optional[Handler] = None, normalize: bool = True, ) -> PathType: """Make a valid file path from a string. To make a valid file path the function does: - 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| - Append underscore (``"_"``) at the tail of the name if sanitized name is one of the reserved names by operating systems (only when ``check_reserved`` is |True|). 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 length of the ``file_path`` length. Truncate the name if the ``file_path`` length exceedd this value. If the value is |None| or minus, ``max_len`` will automatically determined by the ``platform``: - ``Linux``: 4096 - ``macOS``: 1024 - ``Windows``: 260 - ``universal``: 260 check_reserved: If |True|, sanitize reserved names of the ``platform``. null_value_handler: Function called when a value after sanitization is an empty string. Defaults to ``pathvalidate.handler.return_null_string()`` that just return ``""``. normalize: If |True|, normalize the 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` """ return FilePathSanitizer( platform=platform, max_len=-1 if max_len is None else max_len, check_reserved=check_reserved, normalize=normalize, null_value_handler=null_value_handler, ).sanitize(file_path, replacement_text) def sanitize_file_path(file_path, replacement_text="", platform=None, max_path_len=None): # Deprecated return sanitize_filepath(file_path, platform, max_path_len) pathvalidate-2.5.0/pathvalidate/_ltsv.py000066400000000000000000000023671412407513400203440ustar00rootroot00000000000000""" .. codeauthor:: Tsuyoshi Hombashi """ import re from ._common import preprocess, 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, error_msg="label is empty") match_list = __RE_INVALID_LTSV_LABEL.findall(preprocess(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, error_msg="label is empty") return __RE_INVALID_LTSV_LABEL.sub(replacement_text, preprocess(label)) pathvalidate-2.5.0/pathvalidate/_symbol.py000066400000000000000000000057431412407513400206620ustar00rootroot00000000000000""" .. codeauthor:: Tsuyoshi Hombashi """ import re import warnings from typing import Sequence from ._common import ascii_symbols, preprocess, unprintable_ascii_chars from .error import InvalidCharError __RE_UNPRINTABLE = re.compile( "[{}]".format(re.escape("".join(unprintable_ascii_chars))), re.UNICODE ) __RE_SYMBOL = re.compile( "[{}]".format(re.escape("".join(ascii_symbols + unprintable_ascii_chars))), re.UNICODE ) def validate_unprintable(text: str) -> None: # deprecated match_list = __RE_UNPRINTABLE.findall(preprocess(text)) if match_list: raise InvalidCharError(f"unprintable character found: {match_list}") def replace_unprintable(text: str, replacement_text: str = "") -> str: warnings.warn( "'replace_unprintable' has moved to 'replace_unprintable_char'", DeprecationWarning ) try: return __RE_UNPRINTABLE.sub(replacement_text, preprocess(text)) except (TypeError, AttributeError): raise TypeError("text must be a string") 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(preprocess(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, preprocess(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-2.5.0/pathvalidate/argparse.py000066400000000000000000000026741412407513400210220ustar00rootroot00000000000000""" .. codeauthor:: Tsuyoshi Hombashi """ from argparse import ArgumentTypeError from ._common import PathType 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) -> PathType: if not value: return "" return sanitize_filename(value) def sanitize_filepath_arg(value: str) -> PathType: if not value: return "" return sanitize_filepath(value, platform="auto") def filename(value: PathType) -> PathType: # pragma: no cover # Deprecated try: validate_filename(value) except ValidationError as e: raise ArgumentTypeError(e) return sanitize_filename(value) def filepath(value: PathType) -> PathType: # pragma: no cover # Deprecated try: validate_filepath(value) except ValidationError as e: raise ArgumentTypeError(e) return sanitize_filepath(value) pathvalidate-2.5.0/pathvalidate/click.py000066400000000000000000000027561412407513400203040ustar00rootroot00000000000000""" .. codeauthor:: Tsuyoshi Hombashi """ import click from ._common import PathType from ._filename import sanitize_filename, validate_filename from ._filepath import sanitize_filepath, validate_filepath from .error import ValidationError def validate_filename_arg(ctx, param, value) -> 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, param, value) -> 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, param, value) -> PathType: if not value: return "" return sanitize_filename(value) def sanitize_filepath_arg(ctx, param, value) -> PathType: if not value: return "" return sanitize_filepath(value) def filename(ctx, param, value): # pragma: no cover # Deprecated if not value: return None try: validate_filename(value) except ValidationError as e: raise click.BadParameter(str(e)) return sanitize_filename(value) def filepath(ctx, param, value): # pragma: no cover # Deprecated if not value: return None try: validate_filepath(value) except ValidationError as e: raise click.BadParameter(str(e)) return sanitize_filepath(value) pathvalidate-2.5.0/pathvalidate/error.py000066400000000000000000000101171412407513400203360ustar00rootroot00000000000000""" .. codeauthor:: Tsuyoshi Hombashi """ import enum from typing import Optional, cast from ._common import Platform @enum.unique class ErrorReason(enum.Enum): """ Validation error reasons. """ FOUND_ABS_PATH = "FOUND_ABS_PATH" #: found an absolute path when expecting a file name NULL_NAME = "NULL_NAME" #: empty value INVALID_CHARACTER = "INVALID_CHARACTER" #: found invalid characters(s) in a value INVALID_LENGTH = "INVALID_LENGTH" #: found invalid string length MALFORMED_ABS_PATH = "MALFORMED_ABS_PATH" #: found invalid absolute path format RESERVED_NAME = "RESERVED_NAME" #: found a reserved name by a platform class ValidationError(ValueError): """ Exception class of validation errors. .. py:attribute:: reason The cause of the error. Returns: :py:class:`~pathvalidate.error.ErrorReason`: """ @property def platform(self) -> Platform: return self.__platform @property def reason(self) -> Optional[ErrorReason]: return self.__reason @property def description(self) -> str: return self.__description @property def reserved_name(self) -> str: return self.__reserved_name @property def reusable_name(self) -> bool: return self.__reusable_name def __init__(self, *args, **kwargs): self.__platform = kwargs.pop("platform", None) self.__reason = kwargs.pop("reason", None) self.__description = kwargs.pop("description", None) self.__reserved_name = kwargs.pop("reserved_name", None) self.__reusable_name = kwargs.pop("reusable_name", None) try: super().__init__(*args[0], **kwargs) except IndexError: super().__init__(*args, **kwargs) def __str__(self) -> str: item_list = [] if Exception.__str__(self): item_list.append(Exception.__str__(self)) if self.reason: item_list.append(f"reason={cast(ErrorReason, self.reason).value}") if self.platform: item_list.append(f"target-platform={self.platform.value}") if self.description: item_list.append(f"description={self.description}") if self.__reusable_name is not None: item_list.append(f"reusable_name={self.reusable_name}") return ", ".join(item_list).strip() def __repr__(self, *args, **kwargs): return self.__str__(*args, **kwargs) class NullNameError(ValidationError): """ Exception raised when a name is empty. """ def __init__(self, *args, **kwargs) -> None: kwargs["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: kwargs["reason"] = ErrorReason.INVALID_CHARACTER super().__init__(args, **kwargs) class InvalidLengthError(ValidationError): """ Exception raised when a string too long/short. """ def __init__(self, *args, **kwargs) -> None: kwargs["reason"] = ErrorReason.INVALID_LENGTH super().__init__(args, **kwargs) class ReservedNameError(ValidationError): """ Exception raised when a string matched a reserved name. """ def __init__(self, *args, **kwargs) -> None: kwargs["reason"] = ErrorReason.RESERVED_NAME super().__init__(args, **kwargs) class ValidReservedNameError(ReservedNameError): """ Exception raised when a string matched a reserved name. However, it can be used as a name. """ def __init__(self, *args, **kwargs) -> None: kwargs["reusable_name"] = True super().__init__(args, **kwargs) class InvalidReservedNameError(ReservedNameError): """ Exception raised when a string matched a reserved name. Moreover, the reserved name is invalid as a name. """ def __init__(self, *args, **kwargs) -> None: kwargs["reusable_name"] = False super().__init__(args, **kwargs) pathvalidate-2.5.0/pathvalidate/handler.py000066400000000000000000000006601412407513400206240ustar00rootroot00000000000000""" .. codeauthor:: Tsuyoshi Hombashi """ from datetime import datetime from typing import Callable from .error import ValidationError Handler = Callable[[ValidationError], str] def return_null_string(e: ValidationError) -> str: return "" def return_timestamp(e: ValidationError) -> str: return str(datetime.now().timestamp()) def raise_error(e: ValidationError) -> str: raise e pathvalidate-2.5.0/pathvalidate/py.typed000066400000000000000000000000001412407513400203200ustar00rootroot00000000000000pathvalidate-2.5.0/pylama.ini000066400000000000000000000007111412407513400161500ustar00rootroot00000000000000[pylama] skip = .eggs/*,.tox/*,*/.env/*,_sandbox/*,build/*,docs/conf.py [pylama:mccabe] complexity=15 [pylama:pycodestyle] max_line_length = 100 [pylama:pylint] max_line_length = 100 [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-2.5.0/pyproject.toml000066400000000000000000000022721412407513400171040ustar00rootroot00000000000000[build-system] requires = ["setuptools", "wheel"] [tool.black] line-length = 100 exclude = ''' /( \.eggs | \.git | \.mypy_cache | \.tox | \.pytype | \.venv | _build | buck-out | build | dist )/ | docs/conf.py ''' [tool.isort] known_third_party = [ 'allpairspy', 'path', 'pytest', 'readmemaker', 'sphinx_rtd_theme', ] include_trailing_comma = true line_length = 100 lines_after_imports = 2 multi_line_output = 3 skip_glob = [ '*/.eggs/*', '*/.pytype/*', '*/.tox/*', ] [tool.coverage.run] source = ['pathvalidate'] branch = true [tool.coverage.report] show_missing = true precision = 1 exclude_lines = [ 'except ImportError', 'raise NotImplementedError', 'pass', 'ABCmeta', 'abstractmethod', 'abstractproperty', 'abstractclassmethod', 'warnings.warn', ] [tool.mypy] ignore_missing_imports = true python_version = 3.6 pretty = true show_error_codes = true show_error_context = true warn_unreachable = true warn_unused_configs = true warn_unused_ignores = true [tool.pytest.ini_options] testpaths = [ "test", ] md_report = true md_report_verbose = 0 md_report_color = "auto" discord_verbose = 1 pathvalidate-2.5.0/requirements/000077500000000000000000000000001412407513400167105ustar00rootroot00000000000000pathvalidate-2.5.0/requirements/docs_requirements.txt000066400000000000000000000000351412407513400232020ustar00rootroot00000000000000sphinx_rtd_theme Sphinx>=2.4 pathvalidate-2.5.0/requirements/requirements.txt000066400000000000000000000000011412407513400221630ustar00rootroot00000000000000 pathvalidate-2.5.0/requirements/test_requirements.txt000066400000000000000000000001241412407513400232300ustar00rootroot00000000000000allpairspy click faker pytest>=6.0.1 pytest-discord>=0.0.6 pytest-md-report>=0.0.12 pathvalidate-2.5.0/setup.py000066400000000000000000000053731412407513400157070ustar00rootroot00000000000000""" .. codeauthor:: Tsuyoshi Hombashi """ import os.path from typing import Dict 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, 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()] 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", "Changes": f"{REPOSITORY_URL:s}/releases", }, python_requires=">=3.6", extras_require={"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.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "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", ], cmdclass=get_release_command_class(), ) pathvalidate-2.5.0/test/000077500000000000000000000000001412407513400151445ustar00rootroot00000000000000pathvalidate-2.5.0/test/__init__.py000066400000000000000000000000001412407513400172430ustar00rootroot00000000000000pathvalidate-2.5.0/test/_common.py000066400000000000000000000035231412407513400171500ustar00rootroot00000000000000""" .. 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-2.5.0/test/test_argparse.py000066400000000000000000000075501412407513400203700ustar00rootroot00000000000000import 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-2.5.0/test/test_click.py000066400000000000000000000037511412407513400176500ustar00rootroot00000000000000import 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-2.5.0/test/test_common.py000066400000000000000000000027221412407513400200500ustar00rootroot00000000000000""" .. 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-2.5.0/test/test_filename.py000066400000000000000000000462041412407513400203430ustar00rootroot00000000000000""" .. codeauthor:: Tsuyoshi Hombashi """ import platform as m_platform import random 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_filename, sanitize_filename, validate_filename, ) from pathvalidate._common import is_pathlike_obj, unprintable_ascii_chars from pathvalidate._filename import FileNameSanitizer, FileNameValidator from pathvalidate.handler import raise_error, return_null_string, return_timestamp 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 @pytest.mark.parametrize( ["test_platform", "expected"], [ [ "windows", ( "CON", "PRN", "AUX", "CLOCK$", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", ), ], ["linux", ()], ["macos", (":",)], ], ) def test_normal_reserved_keywords(self, test_platform, expected): assert FileNameValidator(255, platform=test_platform).reserved_keywords == expected 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) else: with pytest.raises(ValidationError) as e: validate_filename(value, min_len=min_len) assert e.value.reason == expected @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 @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) else: 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"], [ ["{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( ["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"invalid char found: invalids=('\r'), value='asdf\rsdf', " "reason=INVALID_CHARACTER, target-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"], "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"], "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 is_pathlike_obj(sanitized_name) 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=return_null_string) == "" assert sanitize_filename(value, null_value_handler=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", "check_reserved", "expected"], [ ["CON", True, "CON_"], ["CON", False, "CON"], ], ) def test_normal_check_reserved(self, value, check_reserved, expected): assert ( sanitize_filename(value, platform="windows", check_reserved=check_reserved) == expected ) @pytest.mark.parametrize( ["platform", "value", "expected"], [ ["windows", "period.", "period"], ["windows", "space ", "space"], ["windows", "space_and_period .", "space_and_period"], ["windows", "space_and_period. ", "space_and_period"], ["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"], ], ) 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(["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-2.5.0/test/test_filepath.py000066400000000000000000000664021412407513400203610ustar00rootroot00000000000000""" .. 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 is_pathlike_obj, unprintable_ascii_chars from pathvalidate._filepath import FilePathSanitizer, FilePathValidator from pathvalidate.handler import raise_error, return_null_string, return_timestamp 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 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 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 @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): if expected is None: validate_filepath(value, platform=platform, max_len=max_len) assert is_valid_filepath(value, platform=platform, max_len=max_len) return with pytest.raises(ValidationError) as e: validate_filepath(value, platform=platform, max_len=max_len) assert e.value.reason == ErrorReason.INVALID_LENGTH @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): if expected is None: validate_filepath(value, min_len=min_len, max_len=max_len) assert is_valid_filepath(value, min_len=min_len, max_len=max_len) return with pytest.raises(expected): validate_filepath(value, min_len=min_len, max_len=max_len) @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", "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"invalid char found: invalids=('\r'), value='asdf\rsdf', " "reason=INVALID_CHARACTER, target-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"], "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"], "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", "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", "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 is_pathlike_obj(sanitized_name) 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=return_null_string) == "" assert sanitize_filepath(value, null_value_handler=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): 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(sys.version_info < (3, 6), reason="requires python3.6 or higher") @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-2.5.0/test/test_ltsv.py000066400000000000000000000046111412407513400175470ustar00rootroot00000000000000""" .. 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-2.5.0/test/test_symbol.py000066400000000000000000000102541412407513400200640ustar00rootroot00000000000000""" .. codeauthor:: Tsuyoshi Hombashi """ import itertools import pytest from pathvalidate import ascii_symbols, replace_symbol, unprintable_ascii_chars, validate_symbol from pathvalidate._symbol import validate_unprintable 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: 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(value) @pytest.mark.parametrize(["value"], [["あいうえお"], ["シート"]]) def test_normal_multibyte(self, value): pytest.skip("TODO") validate_unprintable(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(value) assert e.value.reason == ErrorReason.INVALID_CHARACTER pathvalidate-2.5.0/tox.ini000066400000000000000000000030761412407513400155060ustar00rootroot00000000000000[tox] envlist = py{36,37,38,39,310} pypy3 build clean cov docs fmt lint readme [testenv] passenv = * deps = .[test] commands = pytest {posargs} [testenv:build] basepython = python3.8 deps = twine wheel commands = python setup.py sdist bdist_wheel twine check dist/*.whl dist/*.tar.gz python setup.py clean --all [testenv:clean] skip_install = true deps = cleanpy>=0.3 commands = cleanpy --all --exclude-envs . [testenv:cov] deps = .[test] coverage[toml]>=5 commands = coverage run -m pytest {posargs:-vv} coverage report -m [testenv:docs] basepython = python3.8 deps = -r{toxinidir}/requirements/docs_requirements.txt commands = python setup.py build_sphinx --source-dir=docs/ --build-dir=docs/_build --all-files [testenv:fmt] basepython = python3.8 skip_install = true deps = autoflake black 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] basepython = python3.8 skip_install = true deps = codespell mypy>=0.902 pylama pytype types-click commands = python setup.py check mypy pathvalidate setup.py pytype --keep-going --jobs 4 --disable import-error pathvalidate codespell pathvalidate docs/pages examples test -q2 --check-filenames pylama pathvalidate test setup.py [testenv:readme] skip_install = true changedir = docs deps = readmemaker>=1.0.0 commands = python make_readme.py