pax_global_header00006660000000000000000000000064144507216160014520gustar00rootroot0000000000000052 comment=9733e473f6d4f764317ae2bc234b61d0b10daad4 parse_type-0.6.2/000077500000000000000000000000001445072161600137005ustar00rootroot00000000000000parse_type-0.6.2/.bumpversion.cfg000066400000000000000000000002151445072161600170060ustar00rootroot00000000000000[bumpversion] current_version = 0.6.2 files = setup.py parse_type/__init__.py .bumpversion.cfg commit = False tag = False allow_dirty = True parse_type-0.6.2/.coveragerc000066400000000000000000000021741445072161600160250ustar00rootroot00000000000000# ========================================================================= # COVERAGE CONFIGURATION FILE: .coveragerc # ========================================================================= # LANGUAGE: Python # SEE ALSO: # * http://nedbatchelder.com/code/coverage/ # * http://nedbatchelder.com/code/coverage/config.html # ========================================================================= [run] # data_file = .coverage source = parse_type branch = True parallel = True omit = mock.py, ez_setup.py, distribute.py [report] ignore_errors = True show_missing = True # Regexes for lines to exclude from consideration exclude_lines = # Have to re-enable the standard pragma pragma: no cover # Don't complain about missing debug-only code: def __repr__ if self\.debug # Don't complain if tests don't hit defensive assertion code: raise AssertionError raise NotImplementedError # Don't complain if non-runnable code isn't run: if 0: if False: if __name__ == .__main__.: [html] directory = build/coverage.html title = Coverage Report: parse_type [xml] output = build/coverage.xml parse_type-0.6.2/.editorconfig000066400000000000000000000010641445072161600163560ustar00rootroot00000000000000# ============================================================================= # EDITOR CONFIGURATION: http://editorconfig.org # ============================================================================= root = true # -- DEFAULT: Unix-style newlines with a newline ending every file. [*] charset = utf-8 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true [*.{py,rst,ini,txt}] indent_style = space indent_size = 4 [*.feature] indent_style = space indent_size = 2 [**/makefile] indent_style = tab [*.{cmd,bat}] end_of_line = crlf parse_type-0.6.2/.envrc000066400000000000000000000017701445072161600150230ustar00rootroot00000000000000# =========================================================================== # PROJECT ENVIRONMENT SETUP: parse_type/.envrc # =========================================================================== # SHELL: bash (or similiar) # USAGE: # # -- BETTER: Use direnv (requires: Setup in bash -- $HOME/.bashrc) # # BASH PROFILE NEEDS: eval "$(direnv hook bash)" # direnv allow . # # SIMPLISTIC ALTERNATIVE (without cleanup when directory scope is left again): # source .envrc # # SEE ALSO: # * https://direnv.net/ # * https://peps.python.org/pep-0582/ Python local packages directory # =========================================================================== # MAYBE: HERE="${PWD}" # -- USE OPTIONAL PARTS (if exist/enabled): # DISABLED: dotenv_if_exists .env source_env_if_exists .envrc.use_venv # -- SETUP-PYTHON: Prepend ${HERE} to PYTHONPATH (as PRIMARY search path) # SIMILAR TO: export PYTHONPATH="${HERE}:${PYTHONPATH}" path_add PYTHONPATH . # DISABLED: source_env_if_exists .envrc.override parse_type-0.6.2/.envrc.use_venv000066400000000000000000000014671445072161600166570ustar00rootroot00000000000000# =========================================================================== # PROJECT ENVIRONMENT SETUP: parse_type/.envrc.use_venv # =========================================================================== # DESCRIPTION: # Setup and use a Python virtual environment (venv). # On entering the directory: Creates and activates a venv for a python version. # On leaving the directory: Deactivates the venv (virtual environment). # # SEE ALSO: # * https://direnv.net/ # * https://github.com/direnv/direnv/wiki/Python # * https://direnv.net/man/direnv-stdlib.1.html#codelayout-python-ltpythonexegtcode # =========================================================================== # -- VIRTUAL ENVIRONMENT SUPPORT: layout python python3 # VENV LOCATION: .direnv/python-$(PYTHON_VERSION) layout python python3 parse_type-0.6.2/.github/000077500000000000000000000000001445072161600152405ustar00rootroot00000000000000parse_type-0.6.2/.github/workflows/000077500000000000000000000000001445072161600172755ustar00rootroot00000000000000parse_type-0.6.2/.github/workflows/test.yml000066400000000000000000000034221445072161600210000ustar00rootroot00000000000000# -- SOURCE: https://github.com/marketplace/actions/setup-python # SEE: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python # SUPPORTED PYTHON VERSIONS: https://github.com/actions/python-versions name: tests on: workflow_dispatch: push: branches: [ "main", "release/**" ] pull_request: branches: [ "main" ] jobs: test: # -- EXAMPLE: runs-on: ubuntu-latest runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: # PREPARED: os: [ubuntu-latest, macos-latest, windows-latest] # PREPARED: python-version: ['3.9', '2.7', '3.10', '3.8', 'pypy-2.7', 'pypy-3.8'] os: [ubuntu-latest, windows-latest] python-version: ["3.11", "3.10", "3.9", "3.8"] exclude: - os: windows-latest python-version: "2.7" steps: - uses: actions/checkout@v3 # DISABLED: name: Setup Python ${{ matrix.python-version }} on platform=${{ matrix.os }} - uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} cache: 'pip' cache-dependency-path: 'py.requirements/*.txt' # -- DISABLED: # - name: Show Python version # run: python --version - name: Install Python package dependencies run: | python -m pip install -U pip setuptools wheel pip install --upgrade -r py.requirements/ci.github.testing.txt pip install -e . - name: Run tests run: pytest - name: Upload test reports uses: actions/upload-artifact@v3 with: name: test reports path: | build/testing/report.xml build/testing/report.html if: ${{ job.status == 'failure' }} # MAYBE: if: ${{ always() }} parse_type-0.6.2/.gitignore000066400000000000000000000006031445072161600156670ustar00rootroot00000000000000*.py[cod] # -- TEMPORARY PYTHON PACKAGE PARTS: MANIFEST *.egg *.egg-info dist build downloads __pycache__ # Installer logs pip-log.txt Pipfile Pipfile.lock # -- TESTS, COVERAGE REPORTS, ... .cache/ .direnv/ .eggs/ .pytest_cache/ .ruff_cache/ .tox/ .venv*/ .coverage .done.* # -- IDE-RELATED: .fleet/ .idea/ .vscode/ .project .pydevproject # -- EXCLUDE GIT-SUBPROJECTS: /lib/parse/ parse_type-0.6.2/.rosinstall000066400000000000000000000003011445072161600160650ustar00rootroot00000000000000# GIT MULTI-REPO TOOL: wstool # REQUIRES: wstool >= 0.1.17 (better: 0.1.18; not in pypi yet) - git: local-name: lib/parse uri: https://github.com/r1chardj0n3s/parse version: master parse_type-0.6.2/CHANGES.txt000066400000000000000000000063361445072161600155210ustar00rootroot00000000000000Version History =============================================================================== Version: 0.7.0 (UNRELEASED) ------------------------------------------------------------------------------- GOALS: * Drop support for Python 2.7 * Support Python >= 3.7 (probably) Version: 0.6.2 (2023-07-04) ------------------------------------------------------------------------------- FIXES: * #21: tests/test_parse.py tests ``parse_type.parse`` (per default). REASON: Using for older installed ``parse`` module may cause weird problems. RELATED TO: ``parse v1.19.1`` (behavior changed compared to ``v1.19.0``) Version: 0.6.1 (2023-07-02) ------------------------------------------------------------------------------- * Switch to MIT license (same as: `parse`_ module) * Use SPDX-License-Identifier in source code (to simplify understanding) * UPDATE/SYNC to `parse`_ v1.19.1 * ADDED: ``pyproject.toml`` to support newer ``pip`` versions REASON: ``setup.py`` becomes DEPRECATED in 2023-09 for newer ``pip`` versions. FIXED: * Issue #19: 0.6.0: pytest is failing in two units (submitted by: kloczek; caused by: `parse`_ v1.19.1) * Issue #1: Licensing confusion DEVELOPMENT: * VCS: Renamed default branch of Git repository to "main" (was: "master"). * CI: Use github-actions as CI pipeline. Version: 0.6.0 (2022-01-18) ------------------------------------------------------------------------------- FIXED: + issue #17: setup.py: Remove use of "use_2to3" (submitted by: xxx) Version: 0.5.6 (2020-09-11) ------------------------------------------------------------------------------- FIXED: + parse issue #119 (same as: #121): int_convert memory effect with number-base discovery + UPDATE to parse v1.18.0 (needed by: parse issue #119) Version: 0.5.5 (2020-09-10) ------------------------------------------------------------------------------- FIXED: + parse PR #122: Fixes issue #121 in parse: int_convert memory effect. Version: 0.5.4 (2020-09-10) ------------------------------------------------------------------------------- UPDATED: + parse v1.17.0 Version: 0.5.3 (2019-12-15) ------------------------------------------------------------------------------- UPDATED: + setup.py: Add support for Python 3.8. + UPDATE: Dependencies Version: 0.5.2 (2019-07-14) ------------------------------------------------------------------------------- UPDATED: + parse v1.12.0 FIXED: + Python3 DeprecationWarning for regex (here: in docstrings). Version: 0.5.1 (2018-05-27) ------------------------------------------------------------------------------- CHANGED: + Add parse_type.cfparse.Parser(..., case_sensitive=False, ...) parameter to match functionality in parse.Parser constructor (in parse-1.8.4). + UPDATE to parse-1.8.4 Version: 0.5.0 (2018-04-08; includes: v0.4.3) ------------------------------------------------------------------------------- FIXED: + FIX doctest for parse_type.parse module. CHANGES: * UPDATE: parse-1.8.3 (was: parse-1.8.2) NOTE: ``parse`` module and ``parse_type.parse`` module are now identical. BACKWARD INCOMPATIBLE CHANGES: * RENAMED: type_converter.regex_group_count attribute (was: .group_count) (pull-request review changes of the ``parse`` module). .. _parse: https://github.com/r1chardj0n3s/parse parse_type-0.6.2/LICENSE000066400000000000000000000020551445072161600147070ustar00rootroot00000000000000MIT License Copyright (c) 2013-2023 jenisys 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. parse_type-0.6.2/MANIFEST.in000066400000000000000000000007471445072161600154460ustar00rootroot00000000000000include README.rst include LICENSE include .coveragerc include .editorconfig include *.py include *.rst include *.txt include *.ini include *.cfg include *.yaml include bin/invoke* exclude __*.rst exclude __*.txt recursive-include bin *.cmd *.py *.sh recursive-include py.requirements *.txt recursive-include tasks *.py *.txt recursive-include tests *.py # -- DISABLED: recursive-include docs *.rst *.txt *.py prune .direnv prune .tox prune .venv* parse_type-0.6.2/README.rst000066400000000000000000000225321445072161600153730ustar00rootroot00000000000000=============================================================================== parse_type =============================================================================== .. image:: https://github.com/jenisys/parse_type/actions/workflows/test.yml/badge.svg :target: https://github.com/jenisys/parse_type/actions/workflows/test.yml :alt: CI Build Status .. image:: https://img.shields.io/pypi/v/parse_type.svg :target: https://pypi.python.org/pypi/parse_type :alt: Latest Version .. image:: https://img.shields.io/pypi/dm/parse_type.svg :target: https://pypi.python.org/pypi/parse_type :alt: Downloads .. image:: https://img.shields.io/pypi/l/parse_type.svg :target: https://pypi.python.org/pypi/parse_type/ :alt: License `parse_type`_ extends the `parse`_ module (opposite of `string.format()`_) with the following features: * build type converters for common use cases (enum/mapping, choice) * build a type converter with a cardinality constraint (0..1, 0..*, 1..*) from the type converter with cardinality=1. * compose a type converter from other type converters * an extended parser that supports the CardinalityField naming schema and creates missing type variants (0..1, 0..*, 1..*) from the primary type converter .. _parse_type: http://pypi.python.org/pypi/parse_type .. _parse: http://pypi.python.org/pypi/parse .. _`string.format()`: http://docs.python.org/library/string.html#format-string-syntax Definitions ------------------------------------------------------------------------------- *type converter* A type converter function that converts a textual representation of a value type into instance of this value type. In addition, a type converter function is often annotated with attributes that allows the `parse`_ module to use it in a generic way. A type converter is also called a *parse_type* (a definition used here). *cardinality field* A naming convention for related types that differ in cardinality. A cardinality field is a type name suffix in the format of a field. It allows parse format expression, ala:: "{person:Person}" #< Cardinality: 1 (one; the normal case) "{person:Person?}" #< Cardinality: 0..1 (zero or one = optional) "{persons:Person*}" #< Cardinality: 0..* (zero or more = many0) "{persons:Person+}" #< Cardinality: 1..* (one or more = many) This naming convention mimics the relationship descriptions in UML diagrams. Basic Example ------------------------------------------------------------------------------- Define an own type converter for numbers (integers): .. code-block:: python # -- USE CASE: def parse_number(text): return int(text) parse_number.pattern = r"\d+" # -- REGULAR EXPRESSION pattern for type. This is equivalent to: .. code-block:: python import parse @parse.with_pattern(r"\d+") def parse_number(text): return int(text) assert hasattr(parse_number, "pattern") assert parse_number.pattern == r"\d+" .. code-block:: python # -- USE CASE: Use the type converter with the parse module. schema = "Hello {number:Number}" parser = parse.Parser(schema, dict(Number=parse_number)) result = parser.parse("Hello 42") assert result is not None, "REQUIRE: text matches the schema." assert result["number"] == 42 result = parser.parse("Hello XXX") assert result is None, "MISMATCH: text does not match the schema." .. hint:: The described functionality above is standard functionality of the `parse`_ module. It serves as introduction for the remaining cases. Cardinality ------------------------------------------------------------------------------- Create an type converter for "ManyNumbers" (List, separated with commas) with cardinality "1..* = 1+" (many) from the type converter for a "Number". .. code-block:: python # -- USE CASE: Create new type converter with a cardinality constraint. # CARDINALITY: many := one or more (1..*) from parse import Parser from parse_type import TypeBuilder parse_numbers = TypeBuilder.with_many(parse_number, listsep=",") schema = "List: {numbers:ManyNumbers}" parser = Parser(schema, dict(ManyNumbers=parse_numbers)) result = parser.parse("List: 1, 2, 3") assert result["numbers"] == [1, 2, 3] Create an type converter for an "OptionalNumbers" with cardinality "0..1 = ?" (optional) from the type converter for a "Number". .. code-block:: python # -- USE CASE: Create new type converter with cardinality constraint. # CARDINALITY: optional := zero or one (0..1) from parse import Parser from parse_type import TypeBuilder parse_optional_number = TypeBuilder.with_optional(parse_number) schema = "Optional: {number:OptionalNumber}" parser = Parser(schema, dict(OptionalNumber=parse_optional_number)) result = parser.parse("Optional: 42") assert result["number"] == 42 result = parser.parse("Optional: ") assert result["number"] == None Enumeration (Name-to-Value Mapping) ------------------------------------------------------------------------------- Create an type converter for an "Enumeration" from the description of the mapping as dictionary. .. code-block:: python # -- USE CASE: Create a type converter for an enumeration. from parse import Parser from parse_type import TypeBuilder parse_enum_yesno = TypeBuilder.make_enum({"yes": True, "no": False}) parser = Parser("Answer: {answer:YesNo}", dict(YesNo=parse_enum_yesno)) result = parser.parse("Answer: yes") assert result["answer"] == True Create an type converter for an "Enumeration" from the description of the mapping as an enumeration class (`Python 3.4 enum`_ or the `enum34`_ backport; see also: `PEP-0435`_). .. code-block:: python # -- USE CASE: Create a type converter for enum34 enumeration class. # NOTE: Use Python 3.4 or enum34 backport. from parse import Parser from parse_type import TypeBuilder from enum import Enum class Color(Enum): red = 1 green = 2 blue = 3 parse_enum_color = TypeBuilder.make_enum(Color) parser = Parser("Select: {color:Color}", dict(Color=parse_enum_color)) result = parser.parse("Select: red") assert result["color"] is Color.red .. _`Python 3.4 enum`: http://docs.python.org/3.4/library/enum.html#module-enum .. _enum34: http://pypi.python.org/pypi/enum34 .. _PEP-0435: http://www.python.org/dev/peps/pep-0435 Choice (Name Enumeration) ------------------------------------------------------------------------------- A Choice data type allows to select one of several strings. Create an type converter for an "Choice" list, a list of unique names (as string). .. code-block:: python from parse import Parser from parse_type import TypeBuilder parse_choice_yesno = TypeBuilder.make_choice(["yes", "no"]) schema = "Answer: {answer:ChoiceYesNo}" parser = Parser(schema, dict(ChoiceYesNo=parse_choice_yesno)) result = parser.parse("Answer: yes") assert result["answer"] == "yes" Variant (Type Alternatives) ------------------------------------------------------------------------------- Sometimes you need a type converter that can accept text for multiple type converter alternatives. This is normally called a "variant" (or: union). Create an type converter for an "Variant" type that accepts: * Numbers (positive numbers, as integer) * Color enum values (by name) .. code-block:: python from parse import Parser, with_pattern from parse_type import TypeBuilder from enum import Enum class Color(Enum): red = 1 green = 2 blue = 3 @with_pattern(r"\d+") def parse_number(text): return int(text) # -- MAKE VARIANT: Alternatives of different type converters. parse_color = TypeBuilder.make_enum(Color) parse_variant = TypeBuilder.make_variant([parse_number, parse_color]) schema = "Variant: {variant:Number_or_Color}" parser = Parser(schema, dict(Number_or_Color=parse_variant)) # -- TEST VARIANT: With number, color and mismatch. result = parser.parse("Variant: 42") assert result["variant"] == 42 result = parser.parse("Variant: blue") assert result["variant"] is Color.blue result = parser.parse("Variant: __MISMATCH__") assert not result Extended Parser with CardinalityField support ------------------------------------------------------------------------------- The parser extends the ``parse.Parser`` and adds the following functionality: * supports the CardinalityField naming scheme * automatically creates missing type variants for types with a CardinalityField by using the primary type converter for cardinality=1 * extends the provide type converter dictionary with new type variants. Example: .. code-block:: python # -- USE CASE: Parser with CardinalityField support. # NOTE: Automatically adds missing type variants with CardinalityField part. # USE: parse_number() type converter from above. from parse_type.cfparse import Parser # -- PREPARE: parser, adds missing type variant for cardinality 1..* (many) type_dict = dict(Number=parse_number) schema = "List: {numbers:Number+}" parser = Parser(schema, type_dict) assert "Number+" in type_dict, "Created missing type variant based on: Number" # -- USE: parser. result = parser.parse("List: 1, 2, 3") assert result["numbers"] == [1, 2, 3] parse_type-0.6.2/bin/000077500000000000000000000000001445072161600144505ustar00rootroot00000000000000parse_type-0.6.2/bin/invoke000077500000000000000000000002531445072161600156710ustar00rootroot00000000000000#!/bin/sh #!/bin/bash # RUN INVOKE: From bundled ZIP file. HERE=$(dirname $0) export INVOKE_TASKS_USE_VENDOR_BUNDLES="yes" python ${HERE}/../tasks/_vendor/invoke.zip $* parse_type-0.6.2/bin/invoke.cmd000066400000000000000000000003171445072161600164310ustar00rootroot00000000000000@echo off REM RUN INVOKE: From bundled ZIP file. setlocal set HERE=%~dp0 set INVOKE_TASKS_USE_VENDOR_BUNDLES="yes" if not defined PYTHON set PYTHON=python %PYTHON% %HERE%../tasks/_vendor/invoke.zip "%*" parse_type-0.6.2/bin/make_localpi.py000077500000000000000000000170151445072161600174510ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- """ Utility script to create a pypi-like directory structure (localpi) from a number of Python packages in a directory of the local filesystem. DIRECTORY STRUCTURE (before): +-- downloads/ +-- alice-1.0.zip +-- alice-1.0.tar.gz +-- bob-1.3.0.tar.gz +-- bob-1.4.2.tar.gz +-- charly-1.0.tar.bz2 DIRECTORY STRUCTURE (afterwards): +-- downloads/ +-- simple/ | +-- alice/index.html --> ../../alice-*.* | +-- bob/index.html --> ../../bob-*.* | +-- charly/index.html --> ../../charly-*.* | +-- index.html --> alice/, bob/, ... +-- alice-1.0.zip +-- alice-1.0.tar.gz +-- bob-1.3.0.tar.gz +-- bob-1.4.2.tar.gz +-- charly-1.0.tar.bz2 USAGE EXAMPLE: mkdir -p /tmp/downloads pip install --download=/tmp/downloads argparse Jinja2 make_localpi.py /tmp/downloads pip install --index-url=file:///tmp/downloads/simple argparse Jinja2 ALTERNATIVE: pip install --download=/tmp/downloads argparse Jinja2 pip install --find-links=/tmp/downloads --no-index argparse Jinja2 """ from __future__ import with_statement, print_function from fnmatch import fnmatch import os.path import shutil import sys __author__ = "Jens Engel" __version__ = "0.2" __license__ = "BSD" __copyright__ = "(c) 2013 by Jens Engel" class Package(object): """ Package entity that keeps track of: * one or more versions of this package * one or more archive types """ PATTERNS = [ "*.egg", "*.exe", "*.whl", "*.zip", "*.tar.gz", "*.tar.bz2", "*.7z" ] def __init__(self, filename, name=None): if not name and filename: name = self.get_pkgname(filename) self.name = name self.files = [] if filename: self.files.append(filename) @property def versions(self): versions_info = [ self.get_pkgversion(p) for p in self.files ] return versions_info @classmethod def get_pkgversion(cls, filename): parts = os.path.basename(filename).rsplit("-", 1) version = "" if len(parts) >= 2: version = parts[1] for pattern in cls.PATTERNS: assert pattern.startswith("*") suffix = pattern[1:] if version.endswith(suffix): version = version[:-len(suffix)] break return version @staticmethod def get_pkgname(filename): name = os.path.basename(filename).rsplit("-", 1)[0] if name.startswith("http%3A") or name.startswith("https%3A"): # -- PIP DOWNLOAD-CACHE PACKAGE FILE NAME SCHEMA: pos = name.rfind("%2F") name = name[pos+3:] return name @staticmethod def splitext(filename): fname = os.path.splitext(filename)[0] if fname.endswith(".tar"): fname = os.path.splitext(fname)[0] return fname @classmethod def isa(cls, filename): basename = os.path.basename(filename) if basename.startswith("."): return False for pattern in cls.PATTERNS: if fnmatch(filename, pattern): return True return False def make_index_for(package, index_dir, verbose=True): """ Create an 'index.html' for one package. :param package: Package object to use. :param index_dir: Where 'index.html' should be created. """ index_template = """\ {title}

{title}

""" item_template = '
  • {0}
  • ' index_filename = os.path.join(index_dir, "index.html") if not os.path.isdir(index_dir): os.makedirs(index_dir) parts = [] for pkg_filename in package.files: pkg_name = os.path.basename(pkg_filename) if pkg_name == "index.html": # -- ROOT-INDEX: pkg_name = os.path.basename(os.path.dirname(pkg_filename)) else: pkg_name = package.splitext(pkg_name) pkg_relpath_to = os.path.relpath(pkg_filename, index_dir) parts.append(item_template.format(pkg_name, pkg_relpath_to)) if not parts: print("OOPS: Package %s has no files" % package.name) return if verbose: root_index = not Package.isa(package.files[0]) if root_index: info = "with %d package(s)" % len(package.files) else: package_versions = sorted(set(package.versions)) info = ", ".join(reversed(package_versions)) message = "%-30s %s" % (package.name, info) print(message) with open(index_filename, "w") as f: packages = "\n".join(parts) text = index_template.format(title=package.name, packages=packages) f.write(text.strip()) f.close() def make_package_index(download_dir): """ Create a pypi server like file structure below download directory. :param download_dir: Download directory with packages. EXAMPLE BEFORE: +-- downloads/ +-- alice-1.0.zip +-- alice-1.0.tar.gz +-- bob-1.3.0.tar.gz +-- bob-1.4.2.tar.gz +-- charly-1.0.tar.bz2 EXAMPLE AFTERWARDS: +-- downloads/ +-- simple/ | +-- alice/index.html --> ../../alice-*.* | +-- bob/index.html --> ../../bob-*.* | +-- charly/index.html --> ../../charly-*.* | +-- index.html --> alice/index.html, bob/index.html, ... +-- alice-1.0.zip +-- alice-1.0.tar.gz +-- bob-1.3.0.tar.gz +-- bob-1.4.2.tar.gz +-- charly-1.0.tar.bz2 """ if not os.path.isdir(download_dir): raise ValueError("No such directory: %r" % download_dir) pkg_rootdir = os.path.join(download_dir, "simple") if os.path.isdir(pkg_rootdir): shutil.rmtree(pkg_rootdir, ignore_errors=True) os.mkdir(pkg_rootdir) # -- STEP: Collect all packages. package_map = {} packages = [] for filename in sorted(os.listdir(download_dir)): if not Package.isa(filename): continue pkg_filepath = os.path.join(download_dir, filename) package_name = Package.get_pkgname(pkg_filepath) package = package_map.get(package_name, None) if not package: # -- NEW PACKAGE DETECTED: Store/register package. package = Package(pkg_filepath) package_map[package.name] = package packages.append(package) else: # -- SAME PACKAGE: Collect other variant/version. package.files.append(pkg_filepath) # -- STEP: Make local PYTHON PACKAGE INDEX. root_package = Package(None, "Python Package Index") root_package.files = [ os.path.join(pkg_rootdir, pkg.name, "index.html") for pkg in packages ] make_index_for(root_package, pkg_rootdir) for package in packages: index_dir = os.path.join(pkg_rootdir, package.name) make_index_for(package, index_dir) # ----------------------------------------------------------------------------- # MAIN: # ----------------------------------------------------------------------------- if __name__ == "__main__": if (len(sys.argv) != 2) or "-h" in sys.argv[1:] or "--help" in sys.argv[1:]: print("USAGE: %s DOWNLOAD_DIR" % os.path.basename(sys.argv[0])) print(__doc__) sys.exit(1) make_package_index(sys.argv[1]) parse_type-0.6.2/bin/project_bootstrap.sh000077500000000000000000000012401445072161600205470ustar00rootroot00000000000000#!/bin/sh # ============================================================================= # BOOTSTRAP PROJECT: Download all requirements # ============================================================================= # test ${PIP_DOWNLOADS_DIR} || mkdir -p ${PIP_DOWNLOADS_DIR} # tox -e init set -e # -- CONFIGURATION: HERE=`dirname $0` TOP="${HERE}/.." : ${PIP_INDEX_URL="http://pypi.python.org/simple"} : ${PIP_DOWNLOAD_DIR:="${TOP}/downloads"} export PIP_INDEX_URL PIP_DOWNLOADS_DIR # -- EXECUTE STEPS: ${HERE}/toxcmd.py mkdir ${PIP_DOWNLOAD_DIR} pip install --download=${PIP_DOWNLOAD_DIR} -r ${TOP}/requirements/all.txt ${HERE}/make_localpi.py ${PIP_DOWNLOAD_DIR} parse_type-0.6.2/bin/toxcmd.py000077500000000000000000000210721445072161600163250ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: UTF-8 -*- """ Provides a command container for additional tox commands, used in "tox.ini". COMMANDS: * copytree * copy * py2to3 REQUIRES: * argparse """ from glob import glob import argparse import inspect import os.path import shutil import sys __author__ = "Jens Engel" __copyright__ = "(c) 2013 by Jens Engel" __license__ = "BSD" # ----------------------------------------------------------------------------- # CONSTANTS: # ----------------------------------------------------------------------------- VERSION = "0.1.0" FORMATTER_CLASS = argparse.RawDescriptionHelpFormatter # ----------------------------------------------------------------------------- # SUBCOMMAND: copytree # ----------------------------------------------------------------------------- def command_copytree(args): """ Copy one or more source directory(s) below a destination directory. Parts of the destination directory path are created if needed. Similar to the UNIX command: 'cp -R srcdir destdir' """ for srcdir in args.srcdirs: basename = os.path.basename(srcdir) destdir2 = os.path.normpath(os.path.join(args.destdir, basename)) if os.path.exists(destdir2): shutil.rmtree(destdir2) sys.stdout.write("copytree: %s => %s\n" % (srcdir, destdir2)) shutil.copytree(srcdir, destdir2) return 0 def setup_parser_copytree(parser): parser.add_argument("srcdirs", nargs="+", help="Source directory(s)") parser.add_argument("destdir", help="Destination directory") command_copytree.usage = "%(prog)s srcdir... destdir" command_copytree.short = "Copy source dir(s) below a destination directory." command_copytree.setup_parser = setup_parser_copytree # ----------------------------------------------------------------------------- # SUBCOMMAND: copy # ----------------------------------------------------------------------------- def command_copy(args): """ Copy one or more source-files(s) to a destpath (destfile or destdir). Destdir mode is used if: * More than one srcfile is provided * Last parameter ends with a slash ("/"). * Last parameter is an existing directory Destination directory path is created if needed. Similar to the UNIX command: 'cp srcfile... destpath' """ sources = args.sources destpath = args.destpath source_files = [] for file_ in sources: if "*" in file_: selected = glob(file_) source_files.extend(selected) elif os.path.isfile(file_): source_files.append(file_) if destpath.endswith("/") or os.path.isdir(destpath) or len(sources) > 1: # -- DESTDIR-MODE: Last argument is a directory. destdir = destpath else: # -- DESTFILE-MODE: Copy (and rename) one file. assert len(source_files) == 1 destdir = os.path.dirname(destpath) # -- WORK-HORSE: Copy one or more files to destpath. if not os.path.isdir(destdir): sys.stdout.write("copy: Create dir %s\n" % destdir) os.makedirs(destdir) for source in source_files: destname = os.path.join(destdir, os.path.basename(source)) sys.stdout.write("copy: %s => %s\n" % (source, destname)) shutil.copy(source, destname) return 0 def setup_parser_copy(parser): parser.add_argument("sources", nargs="+", help="Source files.") parser.add_argument("destpath", help="Destination path") command_copy.usage = "%(prog)s sources... destpath" command_copy.short = "Copy one or more source files to a destinition." command_copy.setup_parser = setup_parser_copy # ----------------------------------------------------------------------------- # SUBCOMMAND: mkdir # ----------------------------------------------------------------------------- def command_mkdir(args): """ Create a non-existing directory (or more ...). If the directory exists, the step is skipped. Similar to the UNIX command: 'mkdir -p dir' """ errors = 0 for directory in args.dirs: if os.path.exists(directory): if not os.path.isdir(directory): # -- SANITY CHECK: directory exists, but as file... sys.stdout.write("mkdir: %s\n" % directory) sys.stdout.write("ERROR: Exists already, but as file...\n") errors += 1 else: # -- NORMAL CASE: Directory does not exits yet. assert not os.path.isdir(directory) sys.stdout.write("mkdir: %s\n" % directory) os.makedirs(directory) return errors def setup_parser_mkdir(parser): parser.add_argument("dirs", nargs="+", help="Directory(s)") command_mkdir.usage = "%(prog)s dir..." command_mkdir.short = "Create non-existing directory (or more...)." command_mkdir.setup_parser = setup_parser_mkdir # ----------------------------------------------------------------------------- # SUBCOMMAND: py2to3 # ----------------------------------------------------------------------------- def command_py2to3(args): """ Apply '2to3' tool (Python2 to Python3 conversion tool) to Python sources. """ from lib2to3.main import main sys.exit(main("lib2to3.fixes", args=args.sources)) def setup_parser4py2to3(parser): parser.add_argument("sources", nargs="+", help="Source files.") command_py2to3.name = "2to3" command_py2to3.usage = "%(prog)s sources..." command_py2to3.short = "Apply python's 2to3 tool to Python sources." command_py2to3.setup_parser = setup_parser4py2to3 # ----------------------------------------------------------------------------- # COMMAND HELPERS/UTILS: # ----------------------------------------------------------------------------- def discover_commands(): commands = [] for name, func in inspect.getmembers(inspect.getmodule(toxcmd_main)): if name.startswith("__"): continue if name.startswith("command_") and callable(func): command_name0 = name.replace("command_", "") command_name = getattr(func, "name", command_name0) commands.append(Command(command_name, func)) return commands class Command(object): def __init__(self, name, func): assert isinstance(name, basestring) assert callable(func) self.name = name self.func = func self.parser = None def setup_parser(self, command_parser): setup_parser = getattr(self.func, "setup_parser", None) if setup_parser and callable(setup_parser): setup_parser(command_parser) else: command_parser.add_argument("args", nargs="*") @property def usage(self): usage = getattr(self.func, "usage", None) return usage @property def short_description(self): short_description = getattr(self.func, "short", "") return short_description @property def description(self): return inspect.getdoc(self.func) def __call__(self, args): return self.func(args) # ----------------------------------------------------------------------------- # MAIN-COMMAND: # ----------------------------------------------------------------------------- def toxcmd_main(args=None): """Command util with subcommands for tox environments.""" usage = "USAGE: %(prog)s [OPTIONS] COMMAND args..." if args is None: args = sys.argv[1:] # -- STEP: Build command-line parser. parser = argparse.ArgumentParser(description=inspect.getdoc(toxcmd_main), formatter_class=FORMATTER_CLASS) common_parser = parser.add_argument_group("Common options") common_parser.add_argument("--version", action="version", version=VERSION) subparsers = parser.add_subparsers(help="commands") for command in discover_commands(): command_parser = subparsers.add_parser(command.name, usage=command.usage, description=command.description, help=command.short_description, formatter_class=FORMATTER_CLASS) command_parser.set_defaults(func=command) command.setup_parser(command_parser) command.parser = command_parser # -- STEP: Process command-line and run command. options = parser.parse_args(args) command_function = options.func return command_function(options) # ----------------------------------------------------------------------------- # MAIN: # ----------------------------------------------------------------------------- if __name__ == "__main__": sys.exit(toxcmd_main()) parse_type-0.6.2/invoke.yaml000066400000000000000000000012661445072161600160640ustar00rootroot00000000000000# ===================================================== # INVOKE CONFIGURATION: parse_type # ===================================================== # -- ON WINDOWS: # run: # echo: true # pty: false # shell: C:\Windows\System32\cmd.exe # ===================================================== project: name: parse_type repo: "pypi" # -- TODO: until upload problems are resolved. repo_url: "https://upload.pypi.org/legacy/" tasks: auto_dash_names: false run: echo: true cleanup_all: extra_directories: - build - dist - .hypothesis - .pytest_cache - .ruff_cache - ".venv*" - ".tox" extra_files: - ".done.*" parse_type-0.6.2/justfile000066400000000000000000000044561445072161600154610ustar00rootroot00000000000000# ============================================================================= # justfile: A makefile-like build script -- parse_type # ============================================================================= # REQUIRES: cargo install just # PLATFORMS: Windows, Linux, macOS, ... # USAGE: # just --list # just # just # # SEE ALSO: # * https://github.com/casey/just # ============================================================================= # -- OPTION: Load environment-variables from "$HERE/.env" file (if exists) set dotenv-load # ----------------------------------------------------------------------------- # CONFIG: # ----------------------------------------------------------------------------- HERE := justfile_directory() PIP_INSTALL_OPTIONS := env_var_or_default("PIP_INSTALL_OPTIONS", "--quiet") PYTEST_OPTIONS := env_var_or_default("PYTEST_OPTIONS", "") # ----------------------------------------------------------------------------- # BUILD RECIPES / TARGETS: # ----------------------------------------------------------------------------- # DEFAULT-TARGET: Ensure that packages are installed and runs tests. default: (_ensure-install-packages "testing") test # PART=all, testing, ... install-packages PART="all": @echo "INSTALL-PACKAGES: {{PART}} ..." pip install {{PIP_INSTALL_OPTIONS}} -r py.requirements/{{PART}}.txt @touch "{{HERE}}/.done.install-packages.{{PART}}" # ENSURE: Python packages are installed. _ensure-install-packages PART="all": #!/usr/bin/env python3 from subprocess import run from os import path if not path.exists("{{HERE}}/.done.install-packages.{{PART}}"): run("just install-packages {{PART}}", shell=True) # -- SIMILAR: This solution requires a Bourne-like shell (may not work on: Windows). # _ensure-install-packages PART="testing": # @test -e "{{HERE}}/.done.install-packages.{{PART}}" || just install-packages {{PART}} # Run tests. test *TESTS: python -m pytest {{PYTEST_OPTIONS}} {{TESTS}} # Determine test coverage by running the tests. coverage: coverage run -m pytest coverage combine coverage report coverage html # Cleanup most parts (but leave PRECIOUS parts). cleanup: (_ensure-install-packages "all") invoke cleanup # Cleanup everything. cleanup-all: invoke cleanup.all parse_type-0.6.2/parse_type/000077500000000000000000000000001445072161600160535ustar00rootroot00000000000000parse_type-0.6.2/parse_type/__init__.py000066400000000000000000000006621445072161600201700ustar00rootroot00000000000000# -*- coding: UTF-8 -*- # Copyright 2013 - 2023, jenisys # SPDX-License-Identifier: MIT """ This module extends the :mod:`parse` to build and derive additional parse-types from other, existing types. """ from __future__ import absolute_import from parse_type.cardinality import Cardinality from parse_type.builder import TypeBuilder, build_type_dict __all__ = ["Cardinality", "TypeBuilder", "build_type_dict"] __version__ = "0.6.2" parse_type-0.6.2/parse_type/builder.py000066400000000000000000000275771445072161600200750ustar00rootroot00000000000000# -*- coding: utf-8 -*- # pylint: disable=missing-docstring r""" Provides support to compose user-defined parse types. Cardinality ------------ It is often useful to constrain how often a data type occurs. This is also called the cardinality of a data type (in a context). The supported cardinality are: * 0..1 zero_or_one, optional: T or None * 0..N zero_or_more, list_of * 1..N one_or_more, list_of (many) .. doctest:: cardinality >>> from parse_type import TypeBuilder >>> from parse import Parser >>> def parse_number(text): ... return int(text) >>> parse_number.pattern = r"\d+" >>> parse_many_numbers = TypeBuilder.with_many(parse_number) >>> more_types = { "Numbers": parse_many_numbers } >>> parser = Parser("List: {numbers:Numbers}", more_types) >>> parser.parse("List: 1, 2, 3") Enumeration Type (Name-to-Value Mappings) ----------------------------------------- An Enumeration data type allows to select one of several enum values by using its name. The converter function returns the selected enum value. .. doctest:: make_enum >>> parse_enum_yesno = TypeBuilder.make_enum({"yes": True, "no": False}) >>> more_types = { "YesNo": parse_enum_yesno } >>> parser = Parser("Answer: {answer:YesNo}", more_types) >>> parser.parse("Answer: yes") Choice (Name Enumerations) ----------------------------- A Choice data type allows to select one of several strings. .. doctest:: make_choice >>> parse_choice_yesno = TypeBuilder.make_choice(["yes", "no"]) >>> more_types = { "ChoiceYesNo": parse_choice_yesno } >>> parser = Parser("Answer: {answer:ChoiceYesNo}", more_types) >>> parser.parse("Answer: yes") """ from __future__ import absolute_import import inspect import re import enum from parse_type.cardinality import pattern_group_count, \ Cardinality, TypeBuilder as CardinalityTypeBuilder __all__ = ["TypeBuilder", "build_type_dict", "parse_anything"] class TypeBuilder(CardinalityTypeBuilder): """ Provides a utility class to build type-converters (parse_types) for the :mod:`parse` module. """ default_strict = True default_re_opts = (re.IGNORECASE | re.DOTALL) @classmethod def make_list(cls, item_converter=None, listsep=','): """ Create a type converter for a list of items (many := 1..*). The parser accepts anything and the converter needs to fail on errors. :param item_converter: Type converter for an item. :param listsep: List separator to use (as string). :return: Type converter function object for the list. """ if not item_converter: item_converter = parse_anything return cls.with_cardinality(Cardinality.many, item_converter, pattern=cls.anything_pattern, listsep=listsep) @staticmethod def make_enum(enum_mappings): """ Creates a type converter for an enumeration or text-to-value mapping. :param enum_mappings: Defines enumeration names and values. :return: Type converter function object for the enum/mapping. """ if (inspect.isclass(enum_mappings) and issubclass(enum_mappings, enum.Enum)): enum_class = enum_mappings enum_mappings = enum_class.__members__ def convert_enum(text): if text not in convert_enum.mappings: text = text.lower() # REQUIRED-BY: parse re.IGNORECASE return convert_enum.mappings[text] #< text.lower() ??? convert_enum.pattern = r"|".join(enum_mappings.keys()) convert_enum.mappings = enum_mappings return convert_enum @staticmethod def _normalize_choices(choices, transform): assert transform is None or callable(transform) if transform: choices = [transform(value) for value in choices] else: choices = list(choices) return choices @classmethod def make_choice(cls, choices, transform=None, strict=None): """ Creates a type-converter function to select one from a list of strings. The type-converter function returns the selected choice_text. The :param:`transform()` function is applied in the type converter. It can be used to enforce the case (because parser uses re.IGNORECASE). :param choices: List of strings as choice. :param transform: Optional, initial transform function for parsed text. :return: Type converter function object for this choices. """ # -- NOTE: Parser uses re.IGNORECASE flag # => transform may enforce case. choices = cls._normalize_choices(choices, transform) if strict is None: strict = cls.default_strict def convert_choice(text): if transform: text = transform(text) if strict and text not in convert_choice.choices: values = ", ".join(convert_choice.choices) raise ValueError("%s not in: %s" % (text, values)) return text convert_choice.pattern = r"|".join(choices) convert_choice.choices = choices return convert_choice @classmethod def make_choice2(cls, choices, transform=None, strict=None): """ Creates a type converter to select one item from a list of strings. The type converter function returns a tuple (index, choice_text). :param choices: List of strings as choice. :param transform: Optional, initial transform function for parsed text. :return: Type converter function object for this choices. """ choices = cls._normalize_choices(choices, transform) if strict is None: strict = cls.default_strict def convert_choice2(text): if transform: text = transform(text) if strict and text not in convert_choice2.choices: values = ", ".join(convert_choice2.choices) raise ValueError("%s not in: %s" % (text, values)) index = convert_choice2.choices.index(text) return index, text convert_choice2.pattern = r"|".join(choices) convert_choice2.choices = choices return convert_choice2 @classmethod def make_variant(cls, converters, re_opts=None, compiled=False, strict=True): """ Creates a type converter for a number of type converter alternatives. The first matching type converter is used. REQUIRES: type_converter.pattern attribute :param converters: List of type converters as alternatives. :param re_opts: Regular expression options zu use (=default_re_opts). :param compiled: Use compiled regexp matcher, if true (=False). :param strict: Enable assertion checks. :return: Type converter function object. .. note:: Works only with named fields in :class:`parse.Parser`. Parser needs group_index delta for unnamed/fixed fields. This is not supported for user-defined types. Otherwise, you need to use :class:`parse_type.parse.Parser` (patched version of the :mod:`parse` module). """ # -- NOTE: Uses double-dispatch with regex pattern rematch because # match is not passed through to primary type converter. assert converters, "REQUIRE: Non-empty list." if len(converters) == 1: return converters[0] if re_opts is None: re_opts = cls.default_re_opts pattern = r")|(".join([tc.pattern for tc in converters]) pattern = r"("+ pattern + ")" group_count = len(converters) for converter in converters: group_count += pattern_group_count(converter.pattern) if compiled: convert_variant = cls.__create_convert_variant_compiled(converters, re_opts, strict) else: convert_variant = cls.__create_convert_variant(re_opts, strict) convert_variant.pattern = pattern convert_variant.converters = tuple(converters) convert_variant.regex_group_count = group_count return convert_variant @staticmethod def __create_convert_variant(re_opts, strict): # -- USE: Regular expression pattern (compiled on use). def convert_variant(text, m=None): # pylint: disable=invalid-name, unused-argument, missing-docstring for converter in convert_variant.converters: if re.match(converter.pattern, text, re_opts): return converter(text) # -- pragma: no cover assert not strict, "OOPS-VARIANT-MISMATCH: %s" % text return None return convert_variant @staticmethod def __create_convert_variant_compiled(converters, re_opts, strict): # -- USE: Compiled regular expression matcher. for converter in converters: matcher = getattr(converter, "matcher", None) if not matcher: converter.matcher = re.compile(converter.pattern, re_opts) def convert_variant(text, m=None): # pylint: disable=invalid-name, unused-argument, missing-docstring for converter in convert_variant.converters: if converter.matcher.match(text): return converter(text) # -- pragma: no cover assert not strict, "OOPS-VARIANT-MISMATCH: %s" % text return None return convert_variant def build_type_dict(converters): """ Builds type dictionary for user-defined type converters, used by :mod:`parse` module. This requires that each type converter has a "name" attribute. :param converters: List of type converters (parse_types) :return: Type converter dictionary """ more_types = {} for converter in converters: assert callable(converter) more_types[converter.name] = converter return more_types # ----------------------------------------------------------------------------- # COMMON TYPE CONVERTERS # ----------------------------------------------------------------------------- def parse_anything(text, match=None, match_start=0): """ Provides a generic type converter that accepts anything and returns the text (unchanged). :param text: Text to convert (as string). :return: Same text (as string). """ # pylint: disable=unused-argument return text parse_anything.pattern = TypeBuilder.anything_pattern # ----------------------------------------------------------------------------- # Copyright (c) 2012-2020 by Jens Engel (https://github/jenisys/parse_type) # # 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. parse_type-0.6.2/parse_type/cardinality.py000066400000000000000000000205601445072161600207330ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ This module simplifies to build parse types and regular expressions for a data type with the specified cardinality. """ # -- USE: enum34 from __future__ import absolute_import from enum import Enum # ----------------------------------------------------------------------------- # FUNCTIONS: # ----------------------------------------------------------------------------- def pattern_group_count(pattern): """Count the pattern-groups within a regex-pattern (as text).""" return pattern.replace(r"\(", "").count("(") # ----------------------------------------------------------------------------- # CLASS: Cardinality (Enum Class) # ----------------------------------------------------------------------------- class Cardinality(Enum): """Cardinality enumeration class to simplify building regular expression patterns for a data type with the specified cardinality. """ # pylint: disable=bad-whitespace __order__ = "one, zero_or_one, zero_or_more, one_or_more" one = (None, 0) zero_or_one = (r"(%s)?", 1) # SCHEMA: pattern zero_or_more = (r"(%s)?(\s*%s\s*(%s))*", 3) # SCHEMA: pattern sep pattern one_or_more = (r"(%s)(\s*%s\s*(%s))*", 3) # SCHEMA: pattern sep pattern # -- ALIASES: optional = zero_or_one many0 = zero_or_more many = one_or_more def __init__(self, schema, group_count=0): self.schema = schema self.group_count = group_count #< Number of match groups. def is_many(self): """Checks for a more general interpretation of "many". :return: True, if Cardinality.zero_or_more or Cardinality.one_or_more. """ return ((self is Cardinality.zero_or_more) or (self is Cardinality.one_or_more)) def make_pattern(self, pattern, listsep=','): """Make pattern for a data type with the specified cardinality. .. code-block:: python yes_no_pattern = r"yes|no" many_yes_no = Cardinality.one_or_more.make_pattern(yes_no_pattern) :param pattern: Regular expression for type (as string). :param listsep: List separator for multiple items (as string, optional) :return: Regular expression pattern for type with cardinality. """ if self is Cardinality.one: return pattern elif self is Cardinality.zero_or_one: return self.schema % pattern # -- OTHERWISE: return self.schema % (pattern, listsep, pattern) def compute_group_count(self, pattern): """Compute the number of regexp match groups when the pattern is provided to the :func:`Cardinality.make_pattern()` method. :param pattern: Item regexp pattern (as string). :return: Number of regexp match groups in the cardinality pattern. """ group_count = self.group_count pattern_repeated = 1 if self.is_many(): pattern_repeated = 2 return group_count + pattern_repeated * pattern_group_count(pattern) # ----------------------------------------------------------------------------- # CLASS: TypeBuilder # ----------------------------------------------------------------------------- class TypeBuilder(object): """Provides a utility class to build type-converters (parse_types) for parse. It supports to build new type-converters for different cardinality based on the type-converter for cardinality one. """ anything_pattern = r".+?" default_pattern = anything_pattern @classmethod def with_cardinality(cls, cardinality, converter, pattern=None, listsep=','): """Creates a type converter for the specified cardinality by using the type converter for T. :param cardinality: Cardinality to use (0..1, 0..*, 1..*). :param converter: Type converter (function) for data type T. :param pattern: Regexp pattern for an item (=converter.pattern). :return: type-converter for optional (T or None). """ if cardinality is Cardinality.one: return converter # -- NORMAL-CASE builder_func = getattr(cls, "with_%s" % cardinality.name) if cardinality is Cardinality.zero_or_one: return builder_func(converter, pattern) # -- MANY CASE: 0..*, 1..* return builder_func(converter, pattern, listsep=listsep) @classmethod def with_zero_or_one(cls, converter, pattern=None): """Creates a type converter for a T with 0..1 times by using the type converter for one item of T. :param converter: Type converter (function) for data type T. :param pattern: Regexp pattern for an item (=converter.pattern). :return: type-converter for optional (T or None). """ cardinality = Cardinality.zero_or_one if not pattern: pattern = getattr(converter, "pattern", cls.default_pattern) optional_pattern = cardinality.make_pattern(pattern) group_count = cardinality.compute_group_count(pattern) def convert_optional(text, m=None): # pylint: disable=invalid-name, unused-argument, missing-docstring if text: text = text.strip() if not text: return None return converter(text) convert_optional.pattern = optional_pattern convert_optional.regex_group_count = group_count return convert_optional @classmethod def with_zero_or_more(cls, converter, pattern=None, listsep=","): """Creates a type converter function for a list with 0..N items by using the type converter for one item of T. :param converter: Type converter (function) for data type T. :param pattern: Regexp pattern for an item (=converter.pattern). :param listsep: Optional list separator between items (default: ',') :return: type-converter for list """ cardinality = Cardinality.zero_or_more if not pattern: pattern = getattr(converter, "pattern", cls.default_pattern) many0_pattern = cardinality.make_pattern(pattern, listsep) group_count = cardinality.compute_group_count(pattern) def convert_list0(text, m=None): # pylint: disable=invalid-name, unused-argument, missing-docstring if text: text = text.strip() if not text: return [] return [converter(part.strip()) for part in text.split(listsep)] convert_list0.pattern = many0_pattern # OLD convert_list0.group_count = group_count convert_list0.regex_group_count = group_count return convert_list0 @classmethod def with_one_or_more(cls, converter, pattern=None, listsep=","): """Creates a type converter function for a list with 1..N items by using the type converter for one item of T. :param converter: Type converter (function) for data type T. :param pattern: Regexp pattern for an item (=converter.pattern). :param listsep: Optional list separator between items (default: ',') :return: Type converter for list """ cardinality = Cardinality.one_or_more if not pattern: pattern = getattr(converter, "pattern", cls.default_pattern) many_pattern = cardinality.make_pattern(pattern, listsep) group_count = cardinality.compute_group_count(pattern) def convert_list(text, m=None): # pylint: disable=invalid-name, unused-argument, missing-docstring return [converter(part.strip()) for part in text.split(listsep)] convert_list.pattern = many_pattern # OLD: convert_list.group_count = group_count convert_list.regex_group_count = group_count return convert_list # -- ALIAS METHODS: @classmethod def with_optional(cls, converter, pattern=None): """Alias for :py:meth:`with_zero_or_one()` method.""" return cls.with_zero_or_one(converter, pattern) @classmethod def with_many(cls, converter, pattern=None, listsep=','): """Alias for :py:meth:`with_one_or_more()` method.""" return cls.with_one_or_more(converter, pattern, listsep) @classmethod def with_many0(cls, converter, pattern=None, listsep=','): """Alias for :py:meth:`with_zero_or_more()` method.""" return cls.with_zero_or_more(converter, pattern, listsep) parse_type-0.6.2/parse_type/cardinality_field.py000066400000000000000000000152561445072161600221040ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Provides support for cardinality fields. A cardinality field is a type suffix for parse format expression, ala: "{person:Person?}" #< Cardinality: 0..1 = zero or one = optional "{persons:Person*}" #< Cardinality: 0..* = zero or more = many0 "{persons:Person+}" #< Cardinality: 1..* = one or more = many """ from __future__ import absolute_import import six from parse_type.cardinality import Cardinality, TypeBuilder class MissingTypeError(KeyError): # pylint: disable=missing-docstring pass # ----------------------------------------------------------------------------- # CLASS: Cardinality (Field Part) # ----------------------------------------------------------------------------- class CardinalityField(object): """Cardinality field for parse format expression, ala: "{person:Person?}" #< Cardinality: 0..1 = zero or one = optional "{persons:Person*}" #< Cardinality: 0..* = zero or more = many0 "{persons:Person+}" #< Cardinality: 1..* = one or more = many """ # -- MAPPING SUPPORT: pattern_chars = "?*+" from_char_map = { '?': Cardinality.zero_or_one, '*': Cardinality.zero_or_more, '+': Cardinality.one_or_more, } to_char_map = dict([(value, key) for key, value in from_char_map.items()]) @classmethod def matches_type(cls, type_name): """Checks if a type name uses the CardinalityField naming scheme. :param type_name: Type name to check (as string). :return: True, if type name has CardinalityField name suffix. """ return type_name and type_name[-1] in CardinalityField.pattern_chars @classmethod def split_type(cls, type_name): """Split type of a type name with CardinalityField suffix into its parts. :param type_name: Type name (as string). :return: Tuple (type_basename, cardinality) """ if cls.matches_type(type_name): basename = type_name[:-1] cardinality = cls.from_char_map[type_name[-1]] else: # -- ASSUME: Cardinality.one cardinality = Cardinality.one basename = type_name return (basename, cardinality) @classmethod def make_type(cls, basename, cardinality): """Build new type name according to CardinalityField naming scheme. :param basename: Type basename of primary type (as string). :param cardinality: Cardinality of the new type (as Cardinality item). :return: Type name with CardinalityField suffix (if needed) """ if cardinality is Cardinality.one: # -- POSTCONDITION: assert not cls.make_type(type_name) return basename # -- NORMAL CASE: type with CardinalityField suffix. type_name = "%s%s" % (basename, cls.to_char_map[cardinality]) # -- POSTCONDITION: assert cls.make_type(type_name) return type_name # ----------------------------------------------------------------------------- # CLASS: CardinalityFieldTypeBuilder # ----------------------------------------------------------------------------- class CardinalityFieldTypeBuilder(object): """Utility class to create type converters based on: * the CardinalityField naming scheme and * type converter for cardinality=1 """ listsep = ',' @classmethod def create_type_variant(cls, type_name, type_converter): r"""Create type variants for types with a cardinality field. The new type converters are based on the type converter with cardinality=1. .. code-block:: python import parse @parse.with_pattern(r'\d+') def parse_number(text): return int(text) new_type = CardinalityFieldTypeBuilder.create_type_variant( "Number+", parse_number) new_type = CardinalityFieldTypeBuilder.create_type_variant( "Number+", dict(Number=parse_number)) :param type_name: Type name with cardinality field suffix. :param type_converter: Type converter or type dictionary. :return: Type converter variant (function). :raises: ValueError, if type_name does not end with CardinalityField :raises: MissingTypeError, if type_converter is missing in type_dict """ assert isinstance(type_name, six.string_types) if not CardinalityField.matches_type(type_name): message = "type_name='%s' has no CardinalityField" % type_name raise ValueError(message) primary_name, cardinality = CardinalityField.split_type(type_name) if isinstance(type_converter, dict): type_dict = type_converter type_converter = type_dict.get(primary_name, None) if not type_converter: raise MissingTypeError(primary_name) assert callable(type_converter) type_variant = TypeBuilder.with_cardinality(cardinality, type_converter, listsep=cls.listsep) type_variant.name = type_name return type_variant @classmethod def create_type_variants(cls, type_names, type_dict): """Create type variants for types with a cardinality field. The new type converters are based on the type converter with cardinality=1. .. code-block:: python # -- USE: parse_number() type converter function. new_types = CardinalityFieldTypeBuilder.create_type_variants( ["Number?", "Number+"], dict(Number=parse_number)) :param type_names: List of type names with cardinality field suffix. :param type_dict: Type dictionary with named type converters. :return: Type dictionary with type converter variants. """ type_variant_dict = {} for type_name in type_names: type_variant = cls.create_type_variant(type_name, type_dict) type_variant_dict[type_name] = type_variant return type_variant_dict # MAYBE: Check if really needed. @classmethod def create_missing_type_variants(cls, type_names, type_dict): """Create missing type variants for types with a cardinality field. :param type_names: List of type names with cardinality field suffix. :param type_dict: Type dictionary with named type converters. :return: Type dictionary with missing type converter variants. """ missing_type_names = [name for name in type_names if name not in type_dict] return cls.create_type_variants(missing_type_names, type_dict) parse_type-0.6.2/parse_type/cfparse.py000066400000000000000000000072751445072161600200630ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Provides an extended :class:`parse.Parser` class that supports the cardinality fields in (user-defined) types. """ from __future__ import absolute_import import logging import parse from .cardinality_field import CardinalityField, CardinalityFieldTypeBuilder from .parse_util import FieldParser log = logging.getLogger(__name__) # pylint: disable=invalid-name class Parser(parse.Parser): """Provides an extended :class:`parse.Parser` with cardinality field support. A cardinality field is a type suffix for parse format expression, ala: "... {person:Person?} ..." -- OPTIONAL: Cardinality zero or one, 0..1 "... {persons:Person*} ..." -- MANY0: Cardinality zero or more, 0.. "... {persons:Person+} ..." -- MANY: Cardinality one or more, 1.. When the primary type converter for cardinality=1 is provided, the type variants for the other cardinality cases can be derived from it. This parser class automatically creates missing type variants for types with a cardinality field and passes the extended type dictionary to its base class. """ # -- TYPE-BUILDER: For missing types in Fields with CardinalityField part. type_builder = CardinalityFieldTypeBuilder def __init__(self, schema, extra_types=None, case_sensitive=False, type_builder=None): """Creates a parser with CardinalityField part support. :param schema: Parse schema (or format) for parser (as string). :param extra_types: Type dictionary with type converters (or None). :param case_sensitive: Indicates if case-sensitive regexp are used. :param type_builder: Type builder to use for missing types. """ if extra_types is None: extra_types = {} missing = self.create_missing_types(schema, extra_types, type_builder) if missing: # pylint: disable=logging-not-lazy log.debug("MISSING TYPES: %s" % ",".join(missing.keys())) extra_types.update(missing) # -- FINALLY: Delegate to base class. super(Parser, self).__init__(schema, extra_types, case_sensitive=case_sensitive) @classmethod def create_missing_types(cls, schema, type_dict, type_builder=None): """Creates missing types for fields with a CardinalityField part. It is assumed that the primary type converter for cardinality=1 is registered in the type dictionary. :param schema: Parse schema (or format) for parser (as string). :param type_dict: Type dictionary with type converters. :param type_builder: Type builder to use for missing types. :return: Type dictionary with missing types. Empty, if none. :raises: MissingTypeError, if a primary type converter with cardinality=1 is missing. """ if not type_builder: type_builder = cls.type_builder missing = cls.extract_missing_special_type_names(schema, type_dict) return type_builder.create_type_variants(missing, type_dict) @staticmethod def extract_missing_special_type_names(schema, type_dict): # pylint: disable=invalid-name """Extract the type names for fields with CardinalityField part. Selects only the missing type names that are not in the type dictionary. :param schema: Parse schema to use (as string). :param type_dict: Type dictionary with type converters. :return: Generator with missing type names (as string). """ for name in FieldParser.extract_types(schema): if CardinalityField.matches_type(name) and (name not in type_dict): yield name parse_type-0.6.2/parse_type/parse.py000066400000000000000000001032211445072161600175360ustar00rootroot00000000000000# -*- coding: UTF-8 -*- # BASED-ON: https://github.com/r1chardj0n3s/parse/parse.py # VERSION: parse 1.18.0 # Same as original parse modules. # # pylint: disable=line-too-long, invalid-name, too-many-locals, too-many-arguments # pylint: disable=redefined-builtin, too-few-public-methods, no-else-return # pylint: disable=unused-variable, no-self-use, missing-docstring # pylint: disable=unused-argument, unused-variable # pylint: disable=too-many-branches, too-many-statements # pylint: disable=all # # -- ORIGINAL-CODE STARTS-HERE ------------------------------------------------ from __future__ import absolute_import __version__ = '1.19.1' # yes, I now have two problems import re import sys from datetime import datetime, time, tzinfo, timedelta from decimal import Decimal from functools import partial import logging __all__ = 'parse search findall with_pattern'.split() log = logging.getLogger(__name__) def with_pattern(pattern, regex_group_count=None): r"""Attach a regular expression pattern matcher to a custom type converter function. This annotates the type converter with the :attr:`pattern` attribute. EXAMPLE: >>> import parse >>> @parse.with_pattern(r"\d+") ... def parse_number(text): ... return int(text) is equivalent to: >>> def parse_number(text): ... return int(text) >>> parse_number.pattern = r"\d+" :param pattern: regular expression pattern (as text) :param regex_group_count: Indicates how many regex-groups are in pattern. :return: wrapped function """ def decorator(func): func.pattern = pattern func.regex_group_count = regex_group_count return func return decorator class int_convert: """Convert a string to an integer. The string may start with a sign. It may be of a base other than 2, 8, 10 or 16. If base isn't specified, it will be detected automatically based on a string format. When string starts with a base indicator, 0#nnnn, it overrides the default base of 10. It may also have other non-numeric characters that we can ignore. """ CHARS = '0123456789abcdefghijklmnopqrstuvwxyz' def __init__(self, base=None): self.base = base def __call__(self, string, match): if string[0] == '-': sign = -1 number_start = 1 elif string[0] == '+': sign = 1 number_start = 1 else: sign = 1 number_start = 0 base = self.base # If base wasn't specified, detect it automatically if base is None: # Assume decimal number, unless different base is detected base = 10 # For number formats starting with 0b, 0o, 0x, use corresponding base ... if string[number_start] == '0' and len(string) - number_start > 2: if string[number_start + 1] in 'bB': base = 2 elif string[number_start + 1] in 'oO': base = 8 elif string[number_start + 1] in 'xX': base = 16 chars = int_convert.CHARS[:base] string = re.sub('[^%s]' % chars, '', string.lower()) return sign * int(string, base) class convert_first: """Convert the first element of a pair. This equivalent to lambda s,m: converter(s). But unlike a lambda function, it can be pickled """ def __init__(self, converter): self.converter = converter def __call__(self, string, match): return self.converter(string) def percentage(string, match): return float(string[:-1]) / 100.0 class FixedTzOffset(tzinfo): """Fixed offset in minutes east from UTC.""" ZERO = timedelta(0) def __init__(self, offset, name): self._offset = timedelta(minutes=offset) self._name = name def __repr__(self): return '<%s %s %s>' % (self.__class__.__name__, self._name, self._offset) def utcoffset(self, dt): return self._offset def tzname(self, dt): return self._name def dst(self, dt): return self.ZERO def __eq__(self, other): if not isinstance(other, FixedTzOffset): return False return self._name == other._name and self._offset == other._offset MONTHS_MAP = dict( Jan=1, January=1, Feb=2, February=2, Mar=3, March=3, Apr=4, April=4, May=5, Jun=6, June=6, Jul=7, July=7, Aug=8, August=8, Sep=9, September=9, Oct=10, October=10, Nov=11, November=11, Dec=12, December=12, ) DAYS_PAT = r'(Mon|Tue|Wed|Thu|Fri|Sat|Sun)' MONTHS_PAT = r'(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)' ALL_MONTHS_PAT = r'(%s)' % '|'.join(MONTHS_MAP) TIME_PAT = r'(\d{1,2}:\d{1,2}(:\d{1,2}(\.\d+)?)?)' AM_PAT = r'(\s+[AP]M)' TZ_PAT = r'(\s+[-+]\d\d?:?\d\d)' def date_convert( string, match, ymd=None, mdy=None, dmy=None, d_m_y=None, hms=None, am=None, tz=None, mm=None, dd=None, ): """Convert the incoming string containing some date / time info into a datetime instance. """ groups = match.groups() time_only = False if mm and dd: y = datetime.today().year m = groups[mm] d = groups[dd] elif ymd is not None: y, m, d = re.split(r'[-/\s]', groups[ymd]) elif mdy is not None: m, d, y = re.split(r'[-/\s]', groups[mdy]) elif dmy is not None: d, m, y = re.split(r'[-/\s]', groups[dmy]) elif d_m_y is not None: d, m, y = d_m_y d = groups[d] m = groups[m] y = groups[y] else: time_only = True H = M = S = u = 0 if hms is not None and groups[hms]: t = groups[hms].split(':') if len(t) == 2: H, M = t else: H, M, S = t if '.' in S: S, u = S.split('.') u = int(float('.' + u) * 1000000) S = int(S) H = int(H) M = int(M) if am is not None: am = groups[am] if am: am = am.strip() if am == 'AM' and H == 12: # correction for "12" hour functioning as "0" hour: 12:15 AM = 00:15 by 24 hr clock H -= 12 elif am == 'PM' and H == 12: # no correction needed: 12PM is midday, 12:00 by 24 hour clock pass elif am == 'PM': H += 12 if tz is not None: tz = groups[tz] if tz == 'Z': tz = FixedTzOffset(0, 'UTC') elif tz: tz = tz.strip() if tz.isupper(): # TODO use the awesome python TZ module? pass else: sign = tz[0] if ':' in tz: tzh, tzm = tz[1:].split(':') elif len(tz) == 4: # 'snnn' tzh, tzm = tz[1], tz[2:4] else: tzh, tzm = tz[1:3], tz[3:5] offset = int(tzm) + int(tzh) * 60 if sign == '-': offset = -offset tz = FixedTzOffset(offset, tz) if time_only: d = time(H, M, S, u, tzinfo=tz) else: y = int(y) if m.isdigit(): m = int(m) else: m = MONTHS_MAP[m] d = int(d) d = datetime(y, m, d, H, M, S, u, tzinfo=tz) return d class TooManyFields(ValueError): pass class RepeatedNameError(ValueError): pass # note: {} are handled separately # note: I don't use r'' here because Sublime Text 2 syntax highlight has a fit REGEX_SAFETY = re.compile(r'([?\\\\.[\]()*+\^$!\|])') # allowed field types ALLOWED_TYPES = set(list('nbox%fFegwWdDsSl') + ['t' + c for c in 'ieahgcts']) def extract_format(format, extra_types): """Pull apart the format [[fill]align][sign][0][width][.precision][type]""" fill = align = None if format[0] in '<>=^': align = format[0] format = format[1:] elif len(format) > 1 and format[1] in '<>=^': fill = format[0] align = format[1] format = format[2:] if format.startswith(('+', '-', ' ')): format = format[1:] zero = False if format and format[0] == '0': zero = True format = format[1:] width = '' while format: if not format[0].isdigit(): break width += format[0] format = format[1:] if format.startswith('.'): # Precision isn't needed but we need to capture it so that # the ValueError isn't raised. format = format[1:] # drop the '.' precision = '' while format: if not format[0].isdigit(): break precision += format[0] format = format[1:] # the rest is the type, if present type = format if type and type not in ALLOWED_TYPES and type not in extra_types: raise ValueError('format spec %r not recognised' % type) return locals() PARSE_RE = re.compile(r"""({{|}}|{\w*(?:(?:\.\w+)|(?:\[[^\]]+\]))*(?::[^}]+)?})""") class Parser(object): """Encapsulate a format string that may be used to parse other strings.""" def __init__(self, format, extra_types=None, case_sensitive=False): # a mapping of a name as in {hello.world} to a regex-group compatible # name, like hello__world Its used to prevent the transformation of # name-to-group and group to name to fail subtly, such as in: # hello_.world-> hello___world->hello._world self._group_to_name_map = {} # also store the original field name to group name mapping to allow # multiple instances of a name in the format string self._name_to_group_map = {} # and to sanity check the repeated instances store away the first # field type specification for the named field self._name_types = {} self._format = format if extra_types is None: extra_types = {} self._extra_types = extra_types if case_sensitive: self._re_flags = re.DOTALL else: self._re_flags = re.IGNORECASE | re.DOTALL self._fixed_fields = [] self._named_fields = [] self._group_index = 0 self._type_conversions = {} self._expression = self._generate_expression() self.__search_re = None self.__match_re = None log.debug('format %r -> %r', format, self._expression) def __repr__(self): if len(self._format) > 20: return '<%s %r>' % (self.__class__.__name__, self._format[:17] + '...') return '<%s %r>' % (self.__class__.__name__, self._format) @property def _search_re(self): if self.__search_re is None: try: self.__search_re = re.compile(self._expression, self._re_flags) except AssertionError: # access error through sys to keep py3k and backward compat e = str(sys.exc_info()[1]) if e.endswith('this version only supports 100 named groups'): raise TooManyFields( 'sorry, you are attempting to parse ' 'too many complex fields' ) return self.__search_re @property def _match_re(self): if self.__match_re is None: expression = r'\A%s\Z' % self._expression try: self.__match_re = re.compile(expression, self._re_flags) except AssertionError: # access error through sys to keep py3k and backward compat e = str(sys.exc_info()[1]) if e.endswith('this version only supports 100 named groups'): raise TooManyFields( 'sorry, you are attempting to parse ' 'too many complex fields' ) except re.error: raise NotImplementedError( "Group names (e.g. (?P) can " "cause failure, as they are not escaped properly: '%s'" % expression ) return self.__match_re @property def named_fields(self): return self._named_fields.copy() @property def fixed_fields(self): return self._fixed_fields.copy() def parse(self, string, evaluate_result=True): """Match my format to the string exactly. Return a Result or Match instance or None if there's no match. """ m = self._match_re.match(string) if m is None: return None if evaluate_result: return self.evaluate_result(m) else: return Match(self, m) def search(self, string, pos=0, endpos=None, evaluate_result=True): """Search the string for my format. Optionally start the search at "pos" character index and limit the search to a maximum index of endpos - equivalent to search(string[:endpos]). If the ``evaluate_result`` argument is set to ``False`` a Match instance is returned instead of the actual Result instance. Return either a Result instance or None if there's no match. """ if endpos is None: endpos = len(string) m = self._search_re.search(string, pos, endpos) if m is None: return None if evaluate_result: return self.evaluate_result(m) else: return Match(self, m) def findall( self, string, pos=0, endpos=None, extra_types=None, evaluate_result=True ): """Search "string" for all occurrences of "format". Optionally start the search at "pos" character index and limit the search to a maximum index of endpos - equivalent to search(string[:endpos]). Returns an iterator that holds Result or Match instances for each format match found. """ if endpos is None: endpos = len(string) return ResultIterator( self, string, pos, endpos, evaluate_result=evaluate_result ) def _expand_named_fields(self, named_fields): result = {} for field, value in named_fields.items(): # split 'aaa[bbb][ccc]...' into 'aaa' and '[bbb][ccc]...' basename, subkeys = re.match(r'([^\[]+)(.*)', field).groups() # create nested dictionaries {'aaa': {'bbb': {'ccc': ...}}} d = result k = basename if subkeys: for subkey in re.findall(r'\[[^\]]+\]', subkeys): d = d.setdefault(k, {}) k = subkey[1:-1] # assign the value to the last key d[k] = value return result def evaluate_result(self, m): '''Generate a Result instance for the given regex match object''' # ok, figure the fixed fields we've pulled out and type convert them fixed_fields = list(m.groups()) for n in self._fixed_fields: if n in self._type_conversions: fixed_fields[n] = self._type_conversions[n](fixed_fields[n], m) fixed_fields = tuple(fixed_fields[n] for n in self._fixed_fields) # grab the named fields, converting where requested groupdict = m.groupdict() named_fields = {} name_map = {} for k in self._named_fields: korig = self._group_to_name_map[k] name_map[korig] = k if k in self._type_conversions: value = self._type_conversions[k](groupdict[k], m) else: value = groupdict[k] named_fields[korig] = value # now figure the match spans spans = dict((n, m.span(name_map[n])) for n in named_fields) spans.update((i, m.span(n + 1)) for i, n in enumerate(self._fixed_fields)) # and that's our result return Result(fixed_fields, self._expand_named_fields(named_fields), spans) def _regex_replace(self, match): return '\\' + match.group(1) def _generate_expression(self): # turn my _format attribute into the _expression attribute e = [] for part in PARSE_RE.split(self._format): if not part: continue elif part == '{{': e.append(r'\{') elif part == '}}': e.append(r'\}') elif part[0] == '{' and part[-1] == '}': # this will be a braces-delimited field to handle e.append(self._handle_field(part)) else: # just some text to match e.append(REGEX_SAFETY.sub(self._regex_replace, part)) return ''.join(e) def _to_group_name(self, field): # return a version of field which can be used as capture group, even # though it might contain '.' group = field.replace('.', '_').replace('[', '_').replace(']', '_') # make sure we don't collide ("a.b" colliding with "a_b") n = 1 while group in self._group_to_name_map: n += 1 if '.' in field: group = field.replace('.', '_' * n) elif '_' in field: group = field.replace('_', '_' * n) else: raise KeyError('duplicated group name %r' % (field,)) # save off the mapping self._group_to_name_map[group] = field self._name_to_group_map[field] = group return group def _handle_field(self, field): # first: lose the braces field = field[1:-1] # now figure whether this is an anonymous or named field, and whether # there's any format specification format = '' if ':' in field: name, format = field.split(':') else: name = field # This *should* be more flexible, but parsing complicated structures # out of the string is hard (and not necessarily useful) ... and I'm # being lazy. So for now `identifier` is "anything starting with a # letter" and digit args don't get attribute or element stuff. if name and name[0].isalpha(): if name in self._name_to_group_map: if self._name_types[name] != format: raise RepeatedNameError( 'field type %r for field "%s" ' 'does not match previous seen type %r' % (format, name, self._name_types[name]) ) group = self._name_to_group_map[name] # match previously-seen value return r'(?P=%s)' % group else: group = self._to_group_name(name) self._name_types[name] = format self._named_fields.append(group) # this will become a group, which must not contain dots wrap = r'(?P<%s>%%s)' % group else: self._fixed_fields.append(self._group_index) wrap = r'(%s)' group = self._group_index # simplest case: no type specifier ({} or {name}) if not format: self._group_index += 1 return wrap % r'.+?' # decode the format specification format = extract_format(format, self._extra_types) # figure type conversions, if any type = format['type'] is_numeric = type and type in 'n%fegdobx' if type in self._extra_types: type_converter = self._extra_types[type] s = getattr(type_converter, 'pattern', r'.+?') regex_group_count = getattr(type_converter, 'regex_group_count', 0) if regex_group_count is None: regex_group_count = 0 self._group_index += regex_group_count self._type_conversions[group] = convert_first(type_converter) elif type == 'n': s = r'\d{1,3}([,.]\d{3})*' self._group_index += 1 self._type_conversions[group] = int_convert(10) elif type == 'b': s = r'(0[bB])?[01]+' self._type_conversions[group] = int_convert(2) self._group_index += 1 elif type == 'o': s = r'(0[oO])?[0-7]+' self._type_conversions[group] = int_convert(8) self._group_index += 1 elif type == 'x': s = r'(0[xX])?[0-9a-fA-F]+' self._type_conversions[group] = int_convert(16) self._group_index += 1 elif type == '%': s = r'\d+(\.\d+)?%' self._group_index += 1 self._type_conversions[group] = percentage elif type == 'f': s = r'\d*\.\d+' self._type_conversions[group] = convert_first(float) elif type == 'F': s = r'\d*\.\d+' self._type_conversions[group] = convert_first(Decimal) elif type == 'e': s = r'\d*\.\d+[eE][-+]?\d+|nan|NAN|[-+]?inf|[-+]?INF' self._type_conversions[group] = convert_first(float) elif type == 'g': s = r'\d+(\.\d+)?([eE][-+]?\d+)?|nan|NAN|[-+]?inf|[-+]?INF' self._group_index += 2 self._type_conversions[group] = convert_first(float) elif type == 'd': if format.get('width'): width = r'{1,%s}' % int(format['width']) else: width = '+' s = r'\d{w}|[-+ ]?0[xX][0-9a-fA-F]{w}|[-+ ]?0[bB][01]{w}|[-+ ]?0[oO][0-7]{w}'.format( w=width ) self._type_conversions[ group ] = int_convert() # do not specify number base, determine it automatically elif type == 'ti': s = r'(\d{4}-\d\d-\d\d)((\s+|T)%s)?(Z|\s*[-+]\d\d:?\d\d)?' % TIME_PAT n = self._group_index self._type_conversions[group] = partial( date_convert, ymd=n + 1, hms=n + 4, tz=n + 7 ) self._group_index += 7 elif type == 'tg': s = r'(\d{1,2}[-/](\d{1,2}|%s)[-/]\d{4})(\s+%s)?%s?%s?' % ( ALL_MONTHS_PAT, TIME_PAT, AM_PAT, TZ_PAT, ) n = self._group_index self._type_conversions[group] = partial( date_convert, dmy=n + 1, hms=n + 5, am=n + 8, tz=n + 9 ) self._group_index += 9 elif type == 'ta': s = r'((\d{1,2}|%s)[-/]\d{1,2}[-/]\d{4})(\s+%s)?%s?%s?' % ( ALL_MONTHS_PAT, TIME_PAT, AM_PAT, TZ_PAT, ) n = self._group_index self._type_conversions[group] = partial( date_convert, mdy=n + 1, hms=n + 5, am=n + 8, tz=n + 9 ) self._group_index += 9 elif type == 'te': # this will allow microseconds through if they're present, but meh s = r'(%s,\s+)?(\d{1,2}\s+%s\s+\d{4})\s+%s%s' % ( DAYS_PAT, MONTHS_PAT, TIME_PAT, TZ_PAT, ) n = self._group_index self._type_conversions[group] = partial( date_convert, dmy=n + 3, hms=n + 5, tz=n + 8 ) self._group_index += 8 elif type == 'th': # slight flexibility here from the stock Apache format s = r'(\d{1,2}[-/]%s[-/]\d{4}):%s%s' % (MONTHS_PAT, TIME_PAT, TZ_PAT) n = self._group_index self._type_conversions[group] = partial( date_convert, dmy=n + 1, hms=n + 3, tz=n + 6 ) self._group_index += 6 elif type == 'tc': s = r'(%s)\s+%s\s+(\d{1,2})\s+%s\s+(\d{4})' % ( DAYS_PAT, MONTHS_PAT, TIME_PAT, ) n = self._group_index self._type_conversions[group] = partial( date_convert, d_m_y=(n + 4, n + 3, n + 8), hms=n + 5 ) self._group_index += 8 elif type == 'tt': s = r'%s?%s?%s?' % (TIME_PAT, AM_PAT, TZ_PAT) n = self._group_index self._type_conversions[group] = partial( date_convert, hms=n + 1, am=n + 4, tz=n + 5 ) self._group_index += 5 elif type == 'ts': s = r'%s(\s+)(\d+)(\s+)(\d{1,2}:\d{1,2}:\d{1,2})?' % MONTHS_PAT n = self._group_index self._type_conversions[group] = partial( date_convert, mm=n + 1, dd=n + 3, hms=n + 5 ) self._group_index += 5 elif type == 'l': s = r'[A-Za-z]+' elif type: s = r'\%s+' % type elif format.get('precision'): if format.get('width'): s = r'.{%s,%s}?' % (format['width'], format['precision']) else: s = r'.{1,%s}?' % format['precision'] elif format.get('width'): s = r'.{%s,}?' % format['width'] else: s = r'.+?' align = format['align'] fill = format['fill'] # handle some numeric-specific things like fill and sign if is_numeric: # prefix with something (align "=" trumps zero) if align == '=': # special case - align "=" acts like the zero above but with # configurable fill defaulting to "0" if not fill: fill = '0' s = r'%s*' % fill + s # allow numbers to be prefixed with a sign s = r'[-+ ]?' + s if not fill: fill = ' ' # Place into a group now - this captures the value we want to keep. # Everything else from now is just padding to be stripped off if wrap: s = wrap % s self._group_index += 1 if format['width']: # all we really care about is that if the format originally # specified a width then there will probably be padding - without # an explicit alignment that'll mean right alignment with spaces # padding if not align: align = '>' if fill in r'.\+?*[](){}^$': fill = '\\' + fill # align "=" has been handled if align == '<': s = '%s%s*' % (s, fill) elif align == '>': s = '%s*%s' % (fill, s) elif align == '^': s = '%s*%s%s*' % (fill, s, fill) return s class Result(object): """The result of a parse() or search(). Fixed results may be looked up using `result[index]`. Slices of fixed results may also be looked up. Named results may be looked up using `result['name']`. Named results may be tested for existence using `'name' in result`. """ def __init__(self, fixed, named, spans): self.fixed = fixed self.named = named self.spans = spans def __getitem__(self, item): if isinstance(item, (int, slice)): return self.fixed[item] return self.named[item] def __repr__(self): return '<%s %r %r>' % (self.__class__.__name__, self.fixed, self.named) def __contains__(self, name): return name in self.named class Match(object): """The result of a parse() or search() if no results are generated. This class is only used to expose internal used regex match objects to the user and use them for external Parser.evaluate_result calls. """ def __init__(self, parser, match): self.parser = parser self.match = match def evaluate_result(self): '''Generate results for this Match''' return self.parser.evaluate_result(self.match) class ResultIterator(object): """The result of a findall() operation. Each element is a Result instance. """ def __init__(self, parser, string, pos, endpos, evaluate_result=True): self.parser = parser self.string = string self.pos = pos self.endpos = endpos self.evaluate_result = evaluate_result def __iter__(self): return self def __next__(self): m = self.parser._search_re.search(self.string, self.pos, self.endpos) if m is None: raise StopIteration() self.pos = m.end() if self.evaluate_result: return self.parser.evaluate_result(m) else: return Match(self.parser, m) # pre-py3k compat next = __next__ def parse(format, string, extra_types=None, evaluate_result=True, case_sensitive=False): """Using "format" attempt to pull values from "string". The format must match the string contents exactly. If the value you're looking for is instead just a part of the string use search(). If ``evaluate_result`` is True the return value will be an Result instance with two attributes: .fixed - tuple of fixed-position values from the string .named - dict of named values from the string If ``evaluate_result`` is False the return value will be a Match instance with one method: .evaluate_result() - This will return a Result instance like you would get with ``evaluate_result`` set to True The default behaviour is to match strings case insensitively. You may match with case by specifying case_sensitive=True. If the format is invalid a ValueError will be raised. See the module documentation for the use of "extra_types". In the case there is no match parse() will return None. """ p = Parser(format, extra_types=extra_types, case_sensitive=case_sensitive) return p.parse(string, evaluate_result=evaluate_result) def search( format, string, pos=0, endpos=None, extra_types=None, evaluate_result=True, case_sensitive=False, ): """Search "string" for the first occurrence of "format". The format may occur anywhere within the string. If instead you wish for the format to exactly match the string use parse(). Optionally start the search at "pos" character index and limit the search to a maximum index of endpos - equivalent to search(string[:endpos]). If ``evaluate_result`` is True the return value will be an Result instance with two attributes: .fixed - tuple of fixed-position values from the string .named - dict of named values from the string If ``evaluate_result`` is False the return value will be a Match instance with one method: .evaluate_result() - This will return a Result instance like you would get with ``evaluate_result`` set to True The default behaviour is to match strings case insensitively. You may match with case by specifying case_sensitive=True. If the format is invalid a ValueError will be raised. See the module documentation for the use of "extra_types". In the case there is no match parse() will return None. """ p = Parser(format, extra_types=extra_types, case_sensitive=case_sensitive) return p.search(string, pos, endpos, evaluate_result=evaluate_result) def findall( format, string, pos=0, endpos=None, extra_types=None, evaluate_result=True, case_sensitive=False, ): """Search "string" for all occurrences of "format". You will be returned an iterator that holds Result instances for each format match found. Optionally start the search at "pos" character index and limit the search to a maximum index of endpos - equivalent to search(string[:endpos]). If ``evaluate_result`` is True each returned Result instance has two attributes: .fixed - tuple of fixed-position values from the string .named - dict of named values from the string If ``evaluate_result`` is False each returned value is a Match instance with one method: .evaluate_result() - This will return a Result instance like you would get with ``evaluate_result`` set to True The default behaviour is to match strings case insensitively. You may match with case by specifying case_sensitive=True. If the format is invalid a ValueError will be raised. See the module documentation for the use of "extra_types". """ p = Parser(format, extra_types=extra_types, case_sensitive=case_sensitive) return p.findall(string, pos, endpos, evaluate_result=evaluate_result) def compile(format, extra_types=None, case_sensitive=False): """Create a Parser instance to parse "format". The resultant Parser has a method .parse(string) which behaves in the same manner as parse(format, string). The default behaviour is to match strings case insensitively. You may match with case by specifying case_sensitive=True. Use this function if you intend to parse many strings with the same format. See the module documentation for the use of "extra_types". Returns a Parser instance. """ return Parser(format, extra_types=extra_types, case_sensitive=case_sensitive) # Copyright (c) 2012-2020 Richard Jones # # 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. # vim: set filetype=python ts=4 sw=4 et si tw=75 parse_type-0.6.2/parse_type/parse_util.py000066400000000000000000000140401445072161600205730ustar00rootroot00000000000000# -*- coding: utf-8 -*- # pylint: disable=missing-docstring """ Provides generic utility classes for the :class:`parse.Parser` class. """ from __future__ import absolute_import from collections import namedtuple import parse import six # -- HELPER-CLASS: For format part in a Field. # REQUIRES: Python 2.6 or newer. # pylint: disable=redefined-builtin, too-many-arguments FormatSpec = namedtuple("FormatSpec", ["type", "width", "zero", "align", "fill", "precision"]) def make_format_spec(type=None, width="", zero=False, align=None, fill=None, precision=None): return FormatSpec(type, width, zero, align, fill, precision) # pylint: enable=redefined-builtin class Field(object): """ Provides a ValueObject for a Field in a parse expression. Examples: * "{}" * "{name}" * "{:format}" * "{name:format}" Format specification: [[fill]align][0][width][.precision][type] """ # pylint: disable=redefined-builtin ALIGN_CHARS = '<>=^' def __init__(self, name="", format=None): self.name = name self.format = format self._format_spec = None def set_format(self, format): self.format = format self._format_spec = None @property def has_format(self): return bool(self.format) @property def format_spec(self): if not self._format_spec and self.format: self._format_spec = self.extract_format_spec(self.format) return self._format_spec def __str__(self): name = self.name or "" if self.has_format: return "{%s:%s}" % (name, self.format) return "{%s}" % name def __eq__(self, other): if isinstance(other, Field): format1 = self.format or "" format2 = other.format or "" return (self.name == other.name) and (format1 == format2) elif isinstance(other, six.string_types): return str(self) == other else: raise ValueError(other) def __ne__(self, other): return not self.__eq__(other) @staticmethod def make_format(format_spec): """Build format string from a format specification. :param format_spec: Format specification (as FormatSpec object). :return: Composed format (as string). """ fill = '' align = '' zero = '' width = format_spec.width if format_spec.align: align = format_spec.align[0] if format_spec.fill: fill = format_spec.fill[0] if format_spec.zero: zero = '0' precision_part = "" if format_spec.precision: precision_part = ".%s" % format_spec.precision # -- FORMAT-SPEC: [[fill]align][0][width][.precision][type] return "%s%s%s%s%s%s" % (fill, align, zero, width, precision_part, format_spec.type) @classmethod def extract_format_spec(cls, format): """Pull apart the format: [[fill]align][0][width][.precision][type]""" # -- BASED-ON: parse.extract_format() # pylint: disable=redefined-builtin, unsubscriptable-object if not format: raise ValueError("INVALID-FORMAT: %s (empty-string)" % format) orig_format = format fill = align = None if format[0] in cls.ALIGN_CHARS: align = format[0] format = format[1:] elif len(format) > 1 and format[1] in cls.ALIGN_CHARS: fill = format[0] align = format[1] format = format[2:] zero = False if format and format[0] == '0': zero = True format = format[1:] width = '' while format: if not format[0].isdigit(): break width += format[0] format = format[1:] precision = None if format.startswith('.'): # Precision isn't needed but we need to capture it so that # the ValueError isn't raised. format = format[1:] # drop the '.' precision = '' while format: if not format[0].isdigit(): break precision += format[0] format = format[1:] # the rest is the type, if present type = format if not type: raise ValueError("INVALID-FORMAT: %s (without type)" % orig_format) return FormatSpec(type, width, zero, align, fill, precision) class FieldParser(object): """ Utility class that parses/extracts fields in parse expressions. """ @classmethod def parse(cls, text): if not (text.startswith('{') and text.endswith('}')): message = "FIELD-SCHEMA MISMATCH: text='%s' (missing braces)" % text raise ValueError(message) # first: lose the braces text = text[1:-1] if ':' in text: # -- CASE: Typed field with format. name, format_ = text.split(':') else: name = text format_ = None return Field(name, format_) @classmethod def extract_fields(cls, schema): """Extract fields in a parse expression schema. :param schema: Parse expression schema/format to use (as string). :return: Generator for fields in schema (as Field objects). """ # -- BASED-ON: parse.Parser._generate_expression() for part in parse.PARSE_RE.split(schema): if not part or part == '{{' or part == '}}': continue elif part[0] == '{': # this will be a braces-delimited field to handle yield cls.parse(part) @classmethod def extract_types(cls, schema): """Extract types (names) for typed fields (with format/type part). :param schema: Parser schema/format to use. :return: Generator for type names (as string). """ for field in cls.extract_fields(schema): if field.has_format: yield field.format_spec.type parse_type-0.6.2/py.requirements/000077500000000000000000000000001445072161600170525ustar00rootroot00000000000000parse_type-0.6.2/py.requirements/all.txt000066400000000000000000000006611445072161600203660ustar00rootroot00000000000000# ============================================================================ # BEHAVE: PYTHON PACKAGE REQUIREMENTS: All requirements # ============================================================================ # DESCRIPTION: # pip install -r # # SEE ALSO: # * http://www.pip-installer.org/ # ============================================================================ -r basic.txt -r develop.txt -r testing.txt parse_type-0.6.2/py.requirements/basic.txt000066400000000000000000000010161445072161600206720ustar00rootroot00000000000000# ============================================================================ # PYTHON PACKAGE REQUIREMENTS: Normal usage/installation (minimal) # ============================================================================ # DESCRIPTION: # pip install -r # # SEE ALSO: # * http://www.pip-installer.org/ # ============================================================================ parse >= 1.18.0; python_version >= '3.0' parse >= 1.13.1; python_version <= '2.7' enum34; python_version < '3.4' six >= 1.15 parse_type-0.6.2/py.requirements/ci.github.testing.txt000066400000000000000000000006121445072161600231420ustar00rootroot00000000000000pytest < 5.0; python_version < '3.0' pytest >= 5.0; python_version >= '3.0' pytest-html >= 1.19.0 # -- NEEDED: By some tests (as proof of concept) # NOTE: path.py-10.1 is required for python2.6 # HINT: path.py => path (python-install-package was renamed for python3) # DISABLED: path.py >= 11.5.0; python_version < '3.5' # DISABLED: path >= 13.1.0; python_version >= '3.5' -r basic.txt parse_type-0.6.2/py.requirements/develop.txt000066400000000000000000000016131445072161600212520ustar00rootroot00000000000000# ============================================================================ # PYTHON PACKAGE REQUIREMENTS FOR: parse_type -- For development only # ============================================================================ # -- BUILD-SYSTEM SUPPORT: Using invoke -r ../tasks/py.requirements.txt # -- PROJECT ADMIN SUPPORT: bump2version >= 0.5.6 # -- RELEASE MANAGEMENT: Push package to pypi. build >= 0.5.1 twine >= 1.13.0 wheel # -- PYTHON2/PYTHON3 COMPATIBILITY: modernize >= 0.5 # -- LINTERS: ruff; python_version >= '3.7' pylint # -- TEST SUPPORT: CODE COVERAGE SUPPORT, ... coverage >= 4.4 pytest-cov tox >= 1.8.1,<4.0 # -- HINT: tox >= 4.0 has breaking changes. virtualenv < 20.22.0; python_version <= '3.6' # -- SUPPORT FOR: Python 2.7, Python <= 3.6 virtualenv >= 20.0.0; python_version > '3.6' argparse # -- NEEDED-FOR: toxcmd.py # -- RELATED: -r testing.txt -r docs.txt parse_type-0.6.2/py.requirements/docs.txt000066400000000000000000000004231445072161600205420ustar00rootroot00000000000000# ============================================================================ # PYTHON PACKAGE REQUIREMENTS: For documentation generation (PREPARED) # ============================================================================ Sphinx >=1.6 sphinx_bootstrap_theme >= 0.6.0 parse_type-0.6.2/py.requirements/optional.txt000066400000000000000000000005751445072161600214470ustar00rootroot00000000000000# ============================================================================ # PYTHON PACKAGE REQUIREMENTS FOR: parse_type -- Optional for development # ============================================================================ # -- GIT MULTI-REPO TOOL: wstool # REQUIRES: wstool >= 0.1.18 (which is not in pypi.org, yet) https://github.com/vcstools/wstool/archive/0.1.18.zip parse_type-0.6.2/py.requirements/testing.txt000066400000000000000000000005051445072161600212700ustar00rootroot00000000000000# ============================================================================ # PYTHON PACKAGE REQUIREMENTS FOR: parse_type -- For testing only # ============================================================================ pytest < 5.0; python_version < '3.0' pytest >= 5.0; python_version >= '3.0' pytest-html >= 1.19.0 parse_type-0.6.2/pyproject.toml000066400000000000000000000100271445072161600166140ustar00rootroot00000000000000# ============================================================================= # PACKAGING: parse_type # ============================================================================= # SEE: https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html # SEE: https://pypi.org/classifiers/ # MAYBE: requires = ["setuptools", "setuptools-scm"] [build-system] requires = ["setuptools"] build-backend = "setuptools.build_meta" [project] name = "parse_type" authors = [ {name = "Jens Engel", email = "jenisys@noreply.github.com"}, ] description = "Simplifies to build parse types based on the parse module" readme = "README.rst" requires-python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" keywords = ["parse", "parsing"] license = {text = "MIT"} classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Console", "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "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.11", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Code Generators", "Topic :: Software Development :: Libraries :: Python Modules", ] dependencies = [ "parse >= 1.18.0; python_version >= '3.0'", "parse >= 1.13.1; python_version <= '2.7'", "enum34; python_version < '3.4'", "six >= 1.15", ] # PREPARED: dynamic = ["version"] [project.urls] Homepage = "https://github.com/jenisys/parse_type" Download = "http://pypi.python.org/pypi/parse_type" "Source Code" = "https://github.com/jenisys/parse_type" "Issue Tracker" = "https://github.com/jenisys/parse_type/issues/" [project.optional-dependencies] develop = [ "build >= 0.5.1", "twine >= 1.13.0", "coverage >= 4.4", "pytest < 5.0; python_version < '3.0'", # >= 4.2 "pytest >= 5.0; python_version >= '3.0'", "pytest-html >= 1.19.0", "pytest-cov", "tox >=2.8,<4.0", "virtualenv < 20.22.0; python_version <= '3.6'", # -- SUPPORT FOR: Python 2.7, Python <= 3.6 "virtualenv >= 20.0.0; python_version > '3.6'", "ruff; python_version >= '3.7'", "pylint", ] docs = [ "Sphinx >=1.6", "sphinx_bootstrap_theme >= 0.6.0" ] testing = [ "pytest < 5.0; python_version < '3.0'", # >= 4.2 "pytest >= 5.0; python_version >= '3.0'", "pytest-html >= 1.19.0", ] [tool.distutils.bdist_wheel] universal = true # ----------------------------------------------------------------------------- # PACAKING TOOL SPECIFIC PARTS: # ----------------------------------------------------------------------------- [tool.setuptools] platforms = ["any"] zip-safe = true # -- PREPARED: [tool.setuptools.dynamic] version = {attr = "parse_type.__version__"} [tool.setuptools.packages.find] where = ["."] include = ["parse_type*"] exclude = ["tests*"] namespaces = false # ============================================================================= # OTHER TOOLS # ============================================================================= [tool.black] line_length = 100 target-version = ['py38'] include = '\.pyi?$' exclude = ''' ( /( \.git | \.venv | \.netbox | \.vscode | configuration )/ ) ''' [tool.isort] profile = "black" multi_line_output = 3 line_length = 100 # ----------------------------------------------------------------------------- # PYLINT: # ----------------------------------------------------------------------------- [tool.pylint.messages_control] disable = "C0330, C0326" [tool.pylint.format] max-line-length = "100" parse_type-0.6.2/pytest.ini000066400000000000000000000021331445072161600157300ustar00rootroot00000000000000# ============================================================================ # PYTEST CONFIGURATION FILE: pytest.ini # ============================================================================ # SEE ALSO: # * http://pytest.org/ # * http://pytest.org/latest/customize.html # * http://pytest.org/latest/usage.html # * http://pytest.org/latest/example/pythoncollection.html#change-naming-conventions # ============================================================================ # MORE OPTIONS: # addopts = # python_classes=*Test # python_functions=test # ============================================================================ [pytest] minversion = 4.2 testpaths = tests python_files = test_*.py junit_family = xunit2 addopts = --metadata PACKAGE_UNDER_TEST parse_type --html=build/testing/report.html --self-contained-html --junit-xml=build/testing/report.xml # markers = # smoke # slow # -- PREPARED: # filterwarnings = # ignore:.*invalid escape sequence.*:DeprecationWarning # -- BACKWARD COMPATIBILITY: pytest < 2.8 norecursedirs = .git .tox build dist .venv* tmp* _* parse_type-0.6.2/setup.cfg000066400000000000000000000001101445072161600155110ustar00rootroot00000000000000[upload_docs] upload_dir = build/docs/html [bdist_wheel] universal = 1 parse_type-0.6.2/setup.py000066400000000000000000000103671445072161600154210ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- """ Setup script for "parse_type" package. USAGE: python setup.py install # OR: pip install . SEE ALSO: * https://pypi.org/pypi/parse_type * https://github.com/jenisys/parse_type RELATED: * https://setuptools.readthedocs.io/en/latest/history.html """ import sys import os.path sys.path.insert(0, os.curdir) # -- USE: setuptools from setuptools import setup, find_packages # ----------------------------------------------------------------------------- # PREPARE SETUP: # ----------------------------------------------------------------------------- HERE = os.path.dirname(__file__) README = os.path.join(HERE, "README.rst") long_description = ''.join(open(README).readlines()[4:]) # ----------------------------------------------------------------------------- # UTILITY: # ----------------------------------------------------------------------------- def find_packages_by_root_package(where): """Better than excluding everything that is not needed, collect only what is needed. """ root_package = os.path.basename(where) packages = [ "%s.%s" % (root_package, sub_package) for sub_package in find_packages(where)] packages.insert(0, root_package) return packages # ----------------------------------------------------------------------------- # SETUP: # ----------------------------------------------------------------------------- setup( name = "parse_type", version = "0.6.2", author = "Jens Engel", author_email = "jenisys@noreply.github.com", url = "https://github.com/jenisys/parse_type", download_url= "http://pypi.python.org/pypi/parse_type", description = "Simplifies to build parse types based on the parse module", long_description = long_description, keywords= "parse, parsing", license = "MIT", packages = find_packages_by_root_package("parse_type"), include_package_data = True, # -- REQUIREMENTS: python_requires=">=2.7, !=3.0.*, !=3.1.*", install_requires=[ "parse >= 1.18.0; python_version >= '3.0'", "parse >= 1.13.1; python_version <= '2.7'", "enum34; python_version < '3.4'", "six >= 1.15", ], tests_require=[ "pytest < 5.0; python_version < '3.0'", # >= 4.2 "pytest >= 5.0; python_version >= '3.0'", "pytest-html >= 1.19.0", ], extras_require={ 'docs': [ "Sphinx >=1.6", "sphinx_bootstrap_theme >= 0.6.0" ], 'develop': [ "build >= 0.5.1", "twine >= 1.13.0", "coverage >= 4.4", "pytest < 5.0; python_version < '3.0'", # >= 4.2 "pytest >= 5.0; python_version >= '3.0'", "pytest-html >= 1.19.0", "pytest-cov", "tox >=2.8,<4.0", "virtualenv < 20.22.0; python_version <= '3.6'", # -- SUPPORT FOR: Python 2.7, Python <= 3.6 "virtualenv >= 20.0.0; python_version > '3.6'", "ruff; python_version >= '3.7'", "pylint", ], }, test_suite = "tests", test_loader = "setuptools.command.test:ScanningLoader", zip_safe = True, classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Console", "Environment :: Web Environment", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "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.11", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Code Generators", "Topic :: Software Development :: Libraries :: Python Modules", ], platforms = ['any'], ) parse_type-0.6.2/tasks/000077500000000000000000000000001445072161600150255ustar00rootroot00000000000000parse_type-0.6.2/tasks/__init__.py000066400000000000000000000035271445072161600171450ustar00rootroot00000000000000# -*- coding: UTF-8 -*- # pylint: disable=wrong-import-position, wrong-import-order """ Invoke build script. Show all tasks with:: invoke -l .. seealso:: * http://pyinvoke.org * https://github.com/pyinvoke/invoke """ from __future__ import absolute_import, print_function # ----------------------------------------------------------------------------- # IMPORTS: # ----------------------------------------------------------------------------- import sys from invoke import Collection # -- TASK-LIBRARY: import invoke_cleanup as cleanup from . import test from . import release # DISABLED: from . import docs # ----------------------------------------------------------------------------- # TASKS: # ----------------------------------------------------------------------------- # None # ----------------------------------------------------------------------------- # TASK CONFIGURATION: # ----------------------------------------------------------------------------- namespace = Collection() namespace.add_collection(Collection.from_module(cleanup), name="cleanup") namespace.add_collection(Collection.from_module(test)) namespace.add_collection(Collection.from_module(release)) # -- DISABLED: namespace.add_collection(Collection.from_module(docs)) namespace.configure({ "tasks": { "auto_dash_names": False } }) # -- ENSURE: python cleanup is used for this project. cleanup.cleanup_tasks.add_task(cleanup.clean_python) # -- INJECT: clean configuration into this namespace namespace.configure(cleanup.namespace.configuration()) if sys.platform.startswith("win"): # -- OVERRIDE SETTINGS: For platform=win32, ... (Windows) from ._compat_shutil import which run_settings = dict(echo=True, pty=False, shell=which("cmd")) namespace.configure({"run": run_settings}) else: namespace.configure({"run": dict(echo=True, pty=True)}) parse_type-0.6.2/tasks/__main__.py000066400000000000000000000013301445072161600171140ustar00rootroot00000000000000# -*- coding: UTF-8 -*- """ Provides "invoke" script when invoke is not installed. Note that this approach uses the "tasks/_vendor/invoke.zip" bundle package. Usage:: # -- INSTEAD OF: invoke command # Show invoke version python -m tasks --version # List all tasks python -m tasks -l .. seealso:: * http://pyinvoke.org * https://github.com/pyinvoke/invoke """ from __future__ import absolute_import, print_function # ----------------------------------------------------------------------------- # AUTO-MAIN: # ----------------------------------------------------------------------------- if __name__ == "__main__": from invoke.main import program import sys sys.exit(program.run()) parse_type-0.6.2/tasks/_compat_shutil.py000066400000000000000000000003341445072161600204110ustar00rootroot00000000000000# -*- coding: UTF-8 -*- # pylint: disable=unused-import # PYTHON VERSION COMPATIBILITY HELPER try: from shutil import which # -- SINCE: Python 3.3 except ImportError: from backports.shutil_which import which parse_type-0.6.2/tasks/docs.py000066400000000000000000000160171445072161600163340ustar00rootroot00000000000000# -*- coding: UTF-8 -*- """ Provides tasks to build documentation with sphinx, etc. """ from __future__ import absolute_import, print_function import os import sys from invoke import task, Collection from invoke.util import cd from path import Path # -- TASK-LIBRARY: from ._tasklet_cleanup import cleanup_tasks, cleanup_dirs # ----------------------------------------------------------------------------- # CONSTANTS: # ----------------------------------------------------------------------------- SPHINX_LANGUAGE_DEFAULT = os.environ.get("SPHINX_LANGUAGE", "en") # ----------------------------------------------------------------------------- # UTILTITIES: # ----------------------------------------------------------------------------- def _sphinxdoc_get_language(ctx, language=None): language = language or ctx.config.sphinx.language or SPHINX_LANGUAGE_DEFAULT return language def _sphinxdoc_get_destdir(ctx, builder, language=None): if builder == "gettext": # -- CASE: not LANGUAGE-SPECIFIC destdir = Path(ctx.config.sphinx.destdir or "build")/builder else: # -- CASE: LANGUAGE-SPECIFIC: language = _sphinxdoc_get_language(ctx, language) destdir = Path(ctx.config.sphinx.destdir or "build")/builder/language return destdir # ----------------------------------------------------------------------------- # TASKS: # ----------------------------------------------------------------------------- @task def clean(ctx, dry_run=False): """Cleanup generated document artifacts.""" basedir = ctx.sphinx.destdir or "build/docs" cleanup_dirs([basedir], dry_run=dry_run) @task(help={ "builder": "Builder to use (html, ...)", "language": "Language to use (en, ...)", "options": "Additional options for sphinx-build", }) def build(ctx, builder="html", language=None, options=""): """Build docs with sphinx-build""" language = _sphinxdoc_get_language(ctx, language) sourcedir = ctx.config.sphinx.sourcedir destdir = _sphinxdoc_get_destdir(ctx, builder, language=language) destdir = destdir.abspath() with cd(sourcedir): destdir_relative = Path(".").relpathto(destdir) command = "sphinx-build {opts} -b {builder} -D language={language} {sourcedir} {destdir}" \ .format(builder=builder, sourcedir=".", destdir=destdir_relative, language=language, opts=options) ctx.run(command) @task(help={ "builder": "Builder to use (html, ...)", "language": "Language to use (en, ...)", "options": "Additional options for sphinx-build", }) def rebuild(ctx, builder="html", language=None, options=""): """Rebuilds the docs. Perform the steps: clean, build """ clean(ctx) build(ctx, builder=builder, language=None, options=options) @task def linkcheck(ctx): """Check if all links are corect.""" build(ctx, builder="linkcheck") @task(help={"language": "Language to use (en, ...)"}) def browse(ctx, language=None): """Open documentation in web browser.""" output_dir = _sphinxdoc_get_destdir(ctx, "html", language=language) page_html = Path(output_dir)/"index.html" if not page_html.exists(): build(ctx, builder="html") assert page_html.exists() open_cmd = "open" # -- WORKS ON: MACOSX if sys.platform.startswith("win"): open_cmd = "start" ctx.run("{open} {page_html}".format(open=open_cmd, page_html=page_html)) # ctx.run('python -m webbrowser -t {page_html}'.format(page_html=page_html)) # -- DISABLED: # import webbrowser # print("Starting webbrowser with page=%s" % page_html) # webbrowser.open(str(page_html)) @task(help={ "dest": "Destination directory to save docs", "format": "Format/Builder to use (html, ...)", "language": "Language to use (en, ...)", }) # pylint: disable=redefined-builtin def save(ctx, dest="docs.html", format="html", language=None): """Save/update docs under destination directory.""" print("STEP: Generate docs in HTML format") build(ctx, builder=format, language=language) print("STEP: Save docs under %s/" % dest) source_dir = Path(_sphinxdoc_get_destdir(ctx, format, language=language)) Path(dest).rmtree_p() source_dir.copytree(dest) # -- POST-PROCESSING: Polish up. for part in [".buildinfo", ".doctrees"]: partpath = Path(dest)/part if partpath.isdir(): partpath.rmtree_p() elif partpath.exists(): partpath.remove_p() @task(help={ "language": 'Language to use, like "en" (default: "all" to build all).', }) def update_translation(ctx, language="all"): """Update sphinx-doc translation(s) messages from the "English" docs. * Generates gettext *.po files in "build/docs/gettext/" directory * Updates/generates gettext *.po per language in "docs/LOCALE/{language}/" .. note:: Afterwards, the missing message translations can be filled in. :param language: Indicate which language messages to update (or "all"). REQUIRES: * sphinx * sphinx-intl >= 0.9 .. seealso:: https://github.com/sphinx-doc/sphinx-intl """ if language == "all": # -- CASE: Process/update all support languages (translations). DEFAULT_LANGUAGES = os.environ.get("SPHINXINTL_LANGUAGE", None) if DEFAULT_LANGUAGES: # -- EXAMPLE: SPHINXINTL_LANGUAGE="de,ja" DEFAULT_LANGUAGES = DEFAULT_LANGUAGES.split(",") languages = ctx.config.sphinx.languages or DEFAULT_LANGUAGES else: # -- CASE: Process only one language (translation use case). languages = [language] # -- STEP: Generate *.po/*.pot files w/ sphinx-build -b gettext build(ctx, builder="gettext") # -- STEP: Update *.po/*.pot files w/ sphinx-intl if languages: gettext_build_dir = _sphinxdoc_get_destdir(ctx, "gettext").abspath() docs_sourcedir = ctx.config.sphinx.sourcedir languages_opts = "-l "+ " -l ".join(languages) with ctx.cd(docs_sourcedir): ctx.run("sphinx-intl update -p {gettext_dir} {languages}".format( gettext_dir=gettext_build_dir.relpath(docs_sourcedir), languages=languages_opts)) else: print("OOPS: No languages specified (use: SPHINXINTL_LANGUAGE=...)") # ----------------------------------------------------------------------------- # TASK CONFIGURATION: # ----------------------------------------------------------------------------- namespace = Collection(clean, rebuild, linkcheck, browse, save, update_translation) namespace.add_task(build, default=True) namespace.configure({ "sphinx": { # -- FOR TASKS: docs.build, docs.rebuild, docs.clean, ... "language": SPHINX_LANGUAGE_DEFAULT, "sourcedir": "docs", "destdir": "build/docs", # -- FOR TASK: docs.update_translation "languages": None, # -- List of language translations, like: de, ja, ... } }) # -- ADD CLEANUP TASK: cleanup_tasks.add_task(clean, "clean_docs") cleanup_tasks.configure(namespace.configuration()) parse_type-0.6.2/tasks/invoke_dry_run.py000066400000000000000000000032721445072161600204400ustar00rootroot00000000000000# -*- coding: UTF-8 -*- """ Basic support to use a --dry-run mode w/ invoke tasks. .. code-block:: from ._dry_run import DryRunContext @task def destroy_something(ctx, path, dry_run=False): if dry_run: ctx = DryRunContext(ctx) # -- DRY-RUN MODE: Only echos commands. ctx.run("rm -rf {}".format(path)) """ from __future__ import print_function from contextlib import contextmanager @contextmanager def dry_run_mode(ctx): """Contextmanages/scope-guard that switches into dry-run mode. Afterwards the original mode is restored. .. code-block:: python with dry_run_mode(ctx): ctx.run(...) """ # -- SETUP PHASE: initial_dry_run = ctx.config.run.dry ctx.config.run.dry = True yield ctx # -- CLEANUP PHASE: ctx.config.run.dry = initial_dry_run class DryRunContext(object): PREFIX = "DRY-RUN: " SCHEMA = "{prefix}{command}" SCHEMA_WITH_KWARGS = "{prefix}{command} (with kwargs={kwargs})" def __init__(self, ctx=None, prefix=None, schema=None): if prefix is None: prefix = self.PREFIX if schema is None: schema = self.SCHEMA self.ctx = ctx self.prefix = prefix self.schema = schema self.ctx.config.run.dry = True @property def config(self): return self.ctx.config def run(self, command, **kwargs): message = self.schema.format(command=command, prefix=self.prefix, kwargs=kwargs) print(message) def sudo(self, command, **kwargs): command2 = "sudo %s" % command self.run(command2, **kwargs) parse_type-0.6.2/tasks/py.requirements.txt000066400000000000000000000015101445072161600207350ustar00rootroot00000000000000# ============================================================================ # INVOKE PYTHON PACKAGE REQUIREMENTS: For tasks # ============================================================================ # DESCRIPTION: # pip install -r # # SEE ALSO: # * http://www.pip-installer.org/ # ============================================================================ invoke >=1.7.0,<2.0; python_version < '3.6' invoke >=1.7.0; python_version >= '3.6' pycmd six >= 1.15.0 # -- HINT, was RENAMED: path.py => path (for python3) path >= 13.1.0; python_version >= '3.5' path.py >= 11.5.0; python_version < '3.5' # -- PYTHON2 BACKPORTS: pathlib; python_version <= '3.4' backports.shutil_which; python_version <= '3.3' git+https://github.com/jenisys/invoke-cleanup@v0.3.7 # -- SECTION: develop # PREPARED: requests parse_type-0.6.2/tasks/release.py000066400000000000000000000163301445072161600170220ustar00rootroot00000000000000# -*- coding: UTF-8 -*- """ Tasks for releasing this project. Normal steps:: python setup.py sdist bdist_wheel twine register dist/{project}-{version}.tar.gz twine upload dist/* twine upload --skip-existing dist/* python setup.py upload # -- DEPRECATED: No longer supported -> Use RTD instead # -- DEPRECATED: python setup.py upload_docs pypi repositories: * https://pypi.python.org/pypi * https://testpypi.python.org/pypi (not working anymore) * https://test.pypi.org/legacy/ (not working anymore) Configuration file for pypi repositories: .. code-block:: init # -- FILE: $HOME/.pypirc [distutils] index-servers = pypi testpypi [pypi] # DEPRECATED: repository = https://pypi.python.org/pypi username = __USERNAME_HERE__ password: [testpypi] # DEPRECATED: repository = https://test.pypi.org/legacy username = __USERNAME_HERE__ password: .. seealso:: * https://packaging.python.org/ * https://packaging.python.org/guides/ * https://packaging.python.org/tutorials/distributing-packages/ """ from __future__ import absolute_import, print_function from invoke import Collection, task from invoke_cleanup import path_glob from .invoke_dry_run import DryRunContext # ----------------------------------------------------------------------------- # TASKS: # ----------------------------------------------------------------------------- @task def checklist(ctx=None): # pylint: disable=unused-argument """Checklist for releasing this project.""" checklist_text = """PRE-RELEASE CHECKLIST: [ ] Everything is checked in [ ] All tests pass w/ tox RELEASE CHECKLIST: [{x1}] Bump version to new-version and tag repository (via bump_version) [{x2}] Build packages (sdist, bdist_wheel via prepare) [{x3}] Register and upload packages to testpypi repository (first) [{x4}] Verify release is OK and packages from testpypi are usable [{x5}] Register and upload packages to pypi repository [{x6}] Push last changes to Github repository POST-RELEASE CHECKLIST: [ ] Bump version to new-develop-version (via bump_version) [ ] Adapt CHANGES (if necessary) [ ] Commit latest changes to Github repository """ steps = dict(x1=None, x2=None, x3=None, x4=None, x5=None, x6=None) yesno_map = {True: "x", False: "_", None: " "} answers = {name: yesno_map[value] for name, value in steps.items()} print(checklist_text.format(**answers)) @task(name="bump_version") def bump_version(ctx, new_version, version_part=None, dry_run=False): """Bump version (to prepare a new release).""" version_part = version_part or "minor" if dry_run: ctx = DryRunContext(ctx) ctx.run("bumpversion --new-version={} {}".format(new_version, version_part)) @task(name="build", aliases=["build_packages"]) def build_packages(ctx, hide=False): """Build packages for this release.""" print("build_packages:") ctx.run("python setup.py sdist bdist_wheel", echo=True, hide=hide) @task def prepare(ctx, new_version=None, version_part=None, hide=True, dry_run=False): """Prepare the release: bump version, build packages, ...""" if new_version is not None: bump_version(ctx, new_version, version_part=version_part, dry_run=dry_run) build_packages(ctx, hide=hide) packages = ensure_packages_exist(ctx, check_only=True) print_packages(packages) # -- NOT-NEEDED: # @task(name="register") # def register_packages(ctx, repo=None, dry_run=False): # """Register release (packages) in artifact-store/repository.""" # original_ctx = ctx # if repo is None: # repo = ctx.project.repo or "pypi" # if dry_run: # ctx = DryRunContext(ctx) # packages = ensure_packages_exist(original_ctx) # print_packages(packages) # for artifact in packages: # ctx.run("twine register --repository={repo} {artifact}".format( # artifact=artifact, repo=repo)) @task def upload(ctx, repo=None, repo_url=None, dry_run=False, skip_existing=False, verbose=False): """Upload release packages to repository (artifact-store).""" if repo is None: repo = ctx.project.repo or "pypi" if repo_url is None: repo_url = ctx.project.repo_url or None original_ctx = ctx if dry_run: ctx = DryRunContext(ctx) # -- OPTIONS: opts = [] if repo_url: opts.append("--repository-url={0}".format(repo_url)) elif repo: opts.append("--repository={0}".format(repo)) if skip_existing: opts.append("--skip-existing") if verbose: opts.append("--verbose") packages = ensure_packages_exist(original_ctx) print_packages(packages) ctx.run("twine upload {opts} dist/*".format(opts=" ".join(opts))) # ctx.run("twine upload --repository={repo} dist/*".format(repo=repo)) # 2018-05-05 WORK-AROUND for new https://pypi.org/: # twine upload --repository-url=https://upload.pypi.org/legacy /dist/* # NOT-WORKING: repo_url = "https://upload.pypi.org/simple/" # # ctx.run("twine upload --repository-url={repo_url} {opts} dist/*".format( # repo_url=repo_url, opts=" ".join(opts))) # ctx.run("twine upload --repository={repo} {opts} dist/*".format( # repo=repo, opts=" ".join(opts))) # -- DEPRECATED: Use RTD instead # @task(name="upload_docs") # def upload_docs(ctx, repo=None, dry_run=False): # """Upload and publish docs. # # NOTE: Docs are built first. # """ # if repo is None: # repo = ctx.project.repo or "pypi" # if dry_run: # ctx = DryRunContext(ctx) # # ctx.run("python setup.py upload_docs") # # ----------------------------------------------------------------------------- # TASK HELPERS: # ----------------------------------------------------------------------------- def print_packages(packages): print("PACKAGES[%d]:" % len(packages)) for package in packages: package_size = package.stat().st_size package_time = package.stat().st_mtime print(" - %s (size=%s)" % (package, package_size)) def ensure_packages_exist(ctx, pattern=None, check_only=False): if pattern is None: project_name = ctx.project.name project_prefix = project_name.replace("_", "-").split("-")[0] pattern = "dist/%s*" % project_prefix packages = list(path_glob(pattern, current_dir=".")) if not packages: if check_only: message = "No artifacts found: pattern=%s" % pattern raise RuntimeError(message) else: # -- RECURSIVE-SELF-CALL: Once print("NO-PACKAGES-FOUND: Build packages first ...") build_packages(ctx, hide=True) packages = ensure_packages_exist(ctx, pattern, check_only=True) return packages # ----------------------------------------------------------------------------- # TASK CONFIGURATION: # ----------------------------------------------------------------------------- # DISABLED: register_packages namespace = Collection(bump_version, checklist, prepare, build_packages, upload) namespace.configure({ "project": { "repo": "pypi", "repo_url": None, } }) parse_type-0.6.2/tasks/test.py000066400000000000000000000154311445072161600163620ustar00rootroot00000000000000# -*- coding: UTF-8 -*- """ Invoke test tasks. """ from __future__ import print_function import os.path import sys from invoke import task, Collection # -- TASK-LIBRARY: from invoke_cleanup import cleanup_tasks, cleanup_dirs, cleanup_files # --------------------------------------------------------------------------- # CONSTANTS: # --------------------------------------------------------------------------- USE_BEHAVE = False # --------------------------------------------------------------------------- # TASKS # --------------------------------------------------------------------------- @task(name="all", help={ "args": "Command line args for test run.", }) def test_all(ctx, args="", options=""): """Run all tests (default).""" pytest_args = select_by_prefix(args, ctx.pytest.scopes) behave_args = None if USE_BEHAVE: behave_args = select_by_prefix(args, ctx.behave_test.scopes) pytest_should_run = not args or (args and pytest_args) behave_should_run = not args or (args and behave_args) if pytest_should_run: pytest(ctx, pytest_args, options=options) if behave_should_run and USE_BEHAVE: behave(ctx, behave_args, options=options) @task def clean(ctx, dry_run=False): """Cleanup (temporary) test artifacts.""" directories = ctx.test.clean.directories or [] files = ctx.test.clean.files or [] cleanup_dirs(directories, dry_run=dry_run) cleanup_files(files, dry_run=dry_run) @task(name="unit") def unittest(ctx, args="", options=""): """Run unit tests.""" pytest(ctx, args, options) @task def pytest(ctx, args="", options=""): """Run unit tests.""" args = args or ctx.pytest.args options = options or ctx.pytest.options ctx.run("pytest {options} {args}".format(options=options, args=args)) @task(help={ "args": "Command line args for behave", "format": "Formatter to use (progress, pretty, ...)", }) def behave(ctx, args="", format="", options=""): """Run behave tests.""" format = format or ctx.behave_test.format options = options or ctx.behave_test.options args = args or ctx.behave_test.args if os.path.exists("bin/behave"): behave_cmd = "{python} bin/behave".format(python=sys.executable) else: behave_cmd = "{python} -m behave".format(python=sys.executable) for group_args in grouped_by_prefix(args, ctx.behave_test.scopes): ctx.run("{behave} -f {format} {options} {args}".format( behave=behave_cmd, format=format, options=options, args=group_args)) @task(help={ "args": "Tests to run (empty: all)", "report": "Coverage report format to use (report, html, xml)", }) def coverage(ctx, args="", report="report", append=False): """Determine test coverage (run pytest, behave)""" append = append or ctx.coverage.append report_formats = ctx.coverage.report_formats or [] if report not in report_formats: report_formats.insert(0, report) opts = [] if append: opts.append("--append") pytest_args = select_by_prefix(args, ctx.pytest.scopes) behave_args = select_by_prefix(args, ctx.behave_test.scopes) pytest_should_run = not args or (args and pytest_args) behave_should_run = not args or (args and behave_args) and USE_BEHAVE if not args: behave_args = ctx.behave_test.args or "features" if isinstance(pytest_args, list): pytest_args = " ".join(pytest_args) if isinstance(behave_args, list): behave_args = " ".join(behave_args) # -- RUN TESTS WITH COVERAGE: if pytest_should_run: ctx.run("coverage run {options} -m pytest {args}".format( args=pytest_args, options=" ".join(opts))) if behave_should_run and USE_BEHAVE: behave_options = ctx.behave_test.coverage_options or "" os.environ["COVERAGE_PROCESS_START"] = os.path.abspath(".coveragerc") behave(ctx, args=behave_args, options=behave_options) del os.environ["COVERAGE_PROCESS_START"] # -- POST-PROCESSING: ctx.run("coverage combine") for report_format in report_formats: ctx.run("coverage {report_format}".format(report_format=report_format)) # --------------------------------------------------------------------------- # UTILITIES: # --------------------------------------------------------------------------- def select_prefix_for(arg, prefixes): for prefix in prefixes: if arg.startswith(prefix): return prefix return os.path.dirname(arg) def select_by_prefix(args, prefixes): selected = [] for arg in args.strip().split(): assert not arg.startswith("-"), "REQUIRE: arg, not options" scope = select_prefix_for(arg, prefixes) if scope: selected.append(arg) return " ".join(selected) def grouped_by_prefix(args, prefixes): """Group behave args by (directory) scope into multiple test-runs.""" group_args = [] current_scope = None for arg in args.strip().split(): assert not arg.startswith("-"), "REQUIRE: arg, not options" scope = select_prefix_for(arg, prefixes) if scope != current_scope: if group_args: # -- DETECTED GROUP-END: yield " ".join(group_args) group_args = [] current_scope = scope group_args.append(arg) if group_args: yield " ".join(group_args) # --------------------------------------------------------------------------- # TASK MANAGEMENT / CONFIGURATION # --------------------------------------------------------------------------- namespace = Collection(clean, unittest, pytest, coverage) namespace.add_task(test_all, default=True) if USE_BEHAVE: namespace.add_task(behave) namespace.configure({ "test": { "clean": { "directories": [ ".cache", "assets", # -- TEST RUNS # -- BEHAVE-SPECIFIC: "__WORKDIR__", "reports", "test_results", ], "files": [ ".coverage", ".coverage.*", # -- BEHAVE-SPECIFIC: "report.html", "rerun*.txt", "rerun*.featureset", "testrun*.json", ], }, }, "pytest": { "scopes": ["tests"], "args": "", "options": "", # -- NOTE: Overide in configfile "invoke.yaml" }, # "behave_test": behave.namespace._configuration["behave_test"], "behave_test": { "scopes": ["features"], "args": "features", "format": "progress", "options": "", # -- NOTE: Overide in configfile "invoke.yaml" "coverage_options": "", }, "coverage": { "append": False, "report_formats": ["report", "html"], }, }) # -- ADD CLEANUP TASK: cleanup_tasks.add_task(clean, "clean_test") cleanup_tasks.configure(namespace.configuration()) parse_type-0.6.2/tests/000077500000000000000000000000001445072161600150425ustar00rootroot00000000000000parse_type-0.6.2/tests/__init__.py000066400000000000000000000000001445072161600171410ustar00rootroot00000000000000parse_type-0.6.2/tests/parse_type_test.py000077500000000000000000000122001445072161600206240ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import absolute_import from parse_type import TypeBuilder from enum import Enum try: import unittest2 as unittest except ImportError: import unittest # ----------------------------------------------------------------------------- # TEST SUPPORT FOR: TypeBuilder Tests # ----------------------------------------------------------------------------- # -- PROOF-OF-CONCEPT DATATYPE: def parse_number(text): return int(text) parse_number.pattern = r"\d+" # Provide better regexp pattern than default. parse_number.name = "Number" # For testing only. # -- ENUM DATATYPE: parse_yesno = TypeBuilder.make_enum({ "yes": True, "no": False, "on": True, "off": False, "true": True, "false": False, }) parse_yesno.name = "YesNo" # For testing only. # -- ENUM CLASS: class Color(Enum): red = 1 green = 2 blue = 3 parse_color = TypeBuilder.make_enum(Color) parse_color.name = "Color" # -- CHOICE DATATYPE: parse_person_choice = TypeBuilder.make_choice(["Alice", "Bob", "Charly"]) parse_person_choice.name = "PersonChoice" # For testing only. # ----------------------------------------------------------------------------- # ABSTRACT TEST CASE: # ----------------------------------------------------------------------------- class TestCase(unittest.TestCase): # -- PYTHON VERSION BACKWARD-COMPATIBILTY: if not hasattr(unittest.TestCase, "assertIsNone"): def assertIsNone(self, obj, msg=None): self.assert_(obj is None, msg) def assertIsNotNone(self, obj, msg=None): self.assert_(obj is not None, msg) class ParseTypeTestCase(TestCase): """ Common test case base class for :mod:`parse_type` tests. """ def assert_match(self, parser, text, param_name, expected): """ Check that a parser can parse the provided text and extracts the expected value for a parameter. :param parser: Parser to use :param text: Text to parse :param param_name: Name of parameter :param expected: Expected value of parameter. :raise: AssertionError on failures. """ result = parser.parse(text) self.assertIsNotNone(result) self.assertEqual(result[param_name], expected) def assert_mismatch(self, parser, text, param_name=None): """ Check that a parser cannot extract the parameter from the provided text. A parse mismatch has occured. :param parser: Parser to use :param text: Text to parse :param param_name: Name of parameter :raise: AssertionError on failures. """ result = parser.parse(text) self.assertIsNone(result) def ensure_can_parse_all_enum_values(self, parser, type_converter, schema, name): # -- ENSURE: Known enum values are correctly extracted. for value_name, value in type_converter.mappings.items(): text = schema % value_name self.assert_match(parser, text, name, value) def ensure_can_parse_all_choices(self, parser, type_converter, schema, name): transform = getattr(type_converter, "transform", None) for choice_value in type_converter.choices: text = schema % choice_value expected_value = choice_value if transform: assert callable(transform) expected_value = transform(choice_value) self.assert_match(parser, text, name, expected_value) def ensure_can_parse_all_choices2(self, parser, type_converter, schema, name): transform = getattr(type_converter, "transform", None) for index, choice_value in enumerate(type_converter.choices): text = schema % choice_value if transform: assert callable(transform) expected_value = (index, transform(choice_value)) else: expected_value = (index, choice_value) self.assert_match(parser, text, name, expected_value) # Copyright (c) 2012-2013 by Jens Engel (https://github/jenisys/parse_type) # # 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. parse_type-0.6.2/tests/test_builder.py000077500000000000000000000557121445072161600201160ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- """ Test suite for parse_type.py REQUIRES: parse >= 1.8.4 ('pattern' attribute support) """ from __future__ import absolute_import import re import unittest import parse from .parse_type_test import ParseTypeTestCase from .parse_type_test \ import parse_number, parse_yesno, parse_person_choice, parse_color, Color from parse_type import TypeBuilder, build_type_dict from enum import Enum # ----------------------------------------------------------------------------- # TEST CASE: TestTypeBuilder4Enum # ----------------------------------------------------------------------------- class TestTypeBuilder4Enum(ParseTypeTestCase): TYPE_CONVERTERS = [ parse_yesno ] def test_parse_enum_yesno(self): extra_types = build_type_dict([ parse_yesno ]) schema = "Answer: {answer:YesNo}" parser = parse.Parser(schema, extra_types) # -- PERFORM TESTS: self.ensure_can_parse_all_enum_values(parser, parse_yesno, "Answer: %s", "answer") # -- VALID: self.assert_match(parser, "Answer: yes", "answer", True) self.assert_match(parser, "Answer: no", "answer", False) # -- IGNORE-CASE: In parsing, calls type converter function !!! self.assert_match(parser, "Answer: YES", "answer", True) # -- PARSE MISMATCH: self.assert_mismatch(parser, "Answer: __YES__", "answer") self.assert_mismatch(parser, "Answer: yes ", "answer") self.assert_mismatch(parser, "Answer: yes ZZZ", "answer") def test_make_enum_with_dict(self): parse_nword = TypeBuilder.make_enum({"one": 1, "two": 2, "three": 3}) parse_nword.name = "NumberAsWord" extra_types = build_type_dict([ parse_nword ]) schema = "Answer: {number:NumberAsWord}" parser = parse.Parser(schema, extra_types) # -- PERFORM TESTS: self.ensure_can_parse_all_enum_values(parser, parse_nword, "Answer: %s", "number") # -- VALID: self.assert_match(parser, "Answer: one", "number", 1) self.assert_match(parser, "Answer: two", "number", 2) # -- IGNORE-CASE: In parsing, calls type converter function !!! self.assert_match(parser, "Answer: THREE", "number", 3) # -- PARSE MISMATCH: self.assert_mismatch(parser, "Answer: __one__", "number") self.assert_mismatch(parser, "Answer: one ", "number") self.assert_mismatch(parser, "Answer: one_", "number") self.assert_mismatch(parser, "Answer: one ZZZ", "number") def test_make_enum_with_enum_class(self): """ Use :meth:`parse_type.TypeBuilder.make_enum()` with enum34 classes. """ class Color(Enum): red = 1 green = 2 blue = 3 parse_color = TypeBuilder.make_enum(Color) parse_color.name = "Color" schema = "Answer: {color:Color}" parser = parse.Parser(schema, dict(Color=parse_color)) # -- PERFORM TESTS: self.ensure_can_parse_all_enum_values(parser, parse_color, "Answer: %s", "color") # -- VALID: self.assert_match(parser, "Answer: red", "color", Color.red) self.assert_match(parser, "Answer: green", "color", Color.green) self.assert_match(parser, "Answer: blue", "color", Color.blue) # -- IGNORE-CASE: In parsing, calls type converter function !!! self.assert_match(parser, "Answer: RED", "color", Color.red) # -- PARSE MISMATCH: self.assert_mismatch(parser, "Answer: __RED__", "color") self.assert_mismatch(parser, "Answer: red ", "color") self.assert_mismatch(parser, "Answer: redx", "color") self.assert_mismatch(parser, "Answer: redx ZZZ", "color") # ----------------------------------------------------------------------------- # TEST CASE: TestTypeBuilder4Choice # ----------------------------------------------------------------------------- class TestTypeBuilder4Choice(ParseTypeTestCase): def test_parse_choice_persons(self): extra_types = build_type_dict([ parse_person_choice ]) schema = "Answer: {answer:PersonChoice}" parser = parse.Parser(schema, extra_types) # -- PERFORM TESTS: self.assert_match(parser, "Answer: Alice", "answer", "Alice") self.assert_match(parser, "Answer: Bob", "answer", "Bob") self.ensure_can_parse_all_choices(parser, parse_person_choice, "Answer: %s", "answer") # -- IGNORE-CASE: In parsing, calls type converter function !!! # SKIP-WART: self.assert_match(parser, "Answer: BOB", "answer", "BOB") # -- PARSE MISMATCH: self.assert_mismatch(parser, "Answer: __Alice__", "answer") self.assert_mismatch(parser, "Answer: Alice ", "answer") self.assert_mismatch(parser, "Answer: Alice ZZZ", "answer") def test_make_choice(self): parse_choice = TypeBuilder.make_choice(["one", "two", "three"]) parse_choice.name = "NumberWordChoice" extra_types = build_type_dict([ parse_choice ]) schema = "Answer: {answer:NumberWordChoice}" parser = parse.Parser(schema, extra_types) # -- PERFORM TESTS: self.assert_match(parser, "Answer: one", "answer", "one") self.assert_match(parser, "Answer: two", "answer", "two") self.ensure_can_parse_all_choices(parser, parse_choice, "Answer: %s", "answer") # -- PARSE MISMATCH: self.assert_mismatch(parser, "Answer: __one__", "answer") self.assert_mismatch(parser, "Answer: one ", "answer") self.assert_mismatch(parser, "Answer: one ZZZ", "answer") def test_make_choice__anycase_accepted_case_sensitity(self): # -- NOTE: strict=False => Disable errors due to case-mismatch. parse_choice = TypeBuilder.make_choice(["one", "two", "three"], strict=False) schema = "Answer: {answer:NumberWordChoice}" parser = parse.Parser(schema, dict(NumberWordChoice=parse_choice)) # -- PERFORM TESTS: # NOTE: Parser uses re.IGNORECASE flag => Any case accepted. self.assert_match(parser, "Answer: one", "answer", "one") self.assert_match(parser, "Answer: TWO", "answer", "TWO") self.assert_match(parser, "Answer: Three", "answer", "Three") def test_make_choice__samecase_match_or_error(self): # -- NOTE: strict=True => Enable errors due to case-mismatch. parse_choice = TypeBuilder.make_choice(["One", "TWO", "three"], strict=True) schema = "Answer: {answer:NumberWordChoice}" parser = parse.Parser(schema, dict(NumberWordChoice=parse_choice)) # -- PERFORM TESTS: Case matches. # NOTE: Parser uses re.IGNORECASE flag => Any case accepted. self.assert_match(parser, "Answer: One", "answer", "One") self.assert_match(parser, "Answer: TWO", "answer", "TWO") self.assert_match(parser, "Answer: three", "answer", "three") # -- PERFORM TESTS: EXACT-CASE MISMATCH case_mismatch_input_data = ["one", "ONE", "Two", "two", "Three" ] for input_value in case_mismatch_input_data: input_text = "Answer: %s" % input_value with self.assertRaises(ValueError): parser.parse(input_text) def test_make_choice__anycase_accepted_lowercase_enforced(self): # -- NOTE: strict=True => Enable errors due to case-mismatch. parse_choice = TypeBuilder.make_choice(["one", "two", "three"], transform=lambda x: x.lower(), strict=True) schema = "Answer: {answer:NumberWordChoice}" parser = parse.Parser(schema, dict(NumberWordChoice=parse_choice)) # -- PERFORM TESTS: # NOTE: Parser uses re.IGNORECASE flag # => Any case accepted, but result is in lower case. self.assert_match(parser, "Answer: one", "answer", "one") self.assert_match(parser, "Answer: TWO", "answer", "two") self.assert_match(parser, "Answer: Three", "answer", "three") def test_make_choice__with_transform(self): transform = lambda x: x.upper() parse_choice = TypeBuilder.make_choice(["ONE", "two", "Three"], transform) self.assertSequenceEqual(parse_choice.choices, ["ONE", "TWO", "THREE"]) schema = "Answer: {answer:NumberWordChoice}" parser = parse.Parser(schema, dict(NumberWordChoice=parse_choice)) # -- PERFORM TESTS: self.assert_match(parser, "Answer: one", "answer", "ONE") self.assert_match(parser, "Answer: two", "answer", "TWO") self.ensure_can_parse_all_choices(parser, parse_choice, "Answer: %s", "answer") # -- PARSE MISMATCH: self.assert_mismatch(parser, "Answer: __one__", "answer") self.assert_mismatch(parser, "Answer: one ", "answer") self.assert_mismatch(parser, "Answer: one ZZZ", "answer") def test_make_choice2(self): # -- strict=False: Disable errors due to case mismatch. parse_choice2 = TypeBuilder.make_choice2(["zero", "one", "two"], strict=False) parse_choice2.name = "NumberWordChoice2" extra_types = build_type_dict([ parse_choice2 ]) schema = "Answer: {answer:NumberWordChoice2}" parser = parse.Parser(schema, extra_types) # -- PERFORM TESTS: self.assert_match(parser, "Answer: zero", "answer", (0, "zero")) self.assert_match(parser, "Answer: one", "answer", (1, "one")) self.assert_match(parser, "Answer: two", "answer", (2, "two")) self.ensure_can_parse_all_choices2(parser, parse_choice2, "Answer: %s", "answer") # -- PARSE MISMATCH: self.assert_mismatch(parser, "Answer: __one__", "answer") self.assert_mismatch(parser, "Answer: one ", "answer") self.assert_mismatch(parser, "Answer: one ZZZ", "answer") def test_make_choice2__with_transform(self): transform = lambda x: x.lower() parse_choice2 = TypeBuilder.make_choice2(["ZERO", "one", "Two"], transform=transform) self.assertSequenceEqual(parse_choice2.choices, ["zero", "one", "two"]) schema = "Answer: {answer:NumberWordChoice}" parser = parse.Parser(schema, dict(NumberWordChoice=parse_choice2)) # -- PERFORM TESTS: # NOTE: Parser uses re.IGNORECASE => Any case is accepted. self.assert_match(parser, "Answer: zERO", "answer", (0, "zero")) self.assert_match(parser, "Answer: ONE", "answer", (1, "one")) self.assert_match(parser, "Answer: Two", "answer", (2, "two")) def test_make_choice2__samecase_match_or_error(self): # -- NOTE: strict=True => Enable errors due to case-mismatch. parse_choice2 = TypeBuilder.make_choice2(["Zero", "one", "TWO"], strict=True) schema = "Answer: {answer:NumberWordChoice}" parser = parse.Parser(schema, dict(NumberWordChoice=parse_choice2)) # -- PERFORM TESTS: Case matches. # NOTE: Parser uses re.IGNORECASE flag => Any case accepted. self.assert_match(parser, "Answer: Zero", "answer", (0, "Zero")) self.assert_match(parser, "Answer: one", "answer", (1, "one")) self.assert_match(parser, "Answer: TWO", "answer", (2, "TWO")) # -- PERFORM TESTS: EXACT-CASE MISMATCH case_mismatch_input_data = ["zero", "ZERO", "One", "ONE", "two" ] for input_value in case_mismatch_input_data: input_text = "Answer: %s" % input_value with self.assertRaises(ValueError): parser.parse(input_text) # ----------------------------------------------------------------------------- # TEST CASE: TestTypeBuilder4Variant # ----------------------------------------------------------------------------- class TestTypeBuilder4Variant(ParseTypeTestCase): TYPE_CONVERTERS = [ parse_number, parse_yesno ] def check_parse_variant_number_or_yesno(self, parse_variant, with_ignorecase=True): schema = "Variant: {variant:YesNo_or_Number}" parser = parse.Parser(schema, dict(YesNo_or_Number=parse_variant)) # -- TYPE 1: YesNo self.assert_match(parser, "Variant: yes", "variant", True) self.assert_match(parser, "Variant: no", "variant", False) # -- IGNORECASE problem => re_opts if with_ignorecase: self.assert_match(parser, "Variant: YES", "variant", True) # -- TYPE 2: Number self.assert_match(parser, "Variant: 0", "variant", 0) self.assert_match(parser, "Variant: 1", "variant", 1) self.assert_match(parser, "Variant: 12", "variant", 12) self.assert_match(parser, "Variant: 42", "variant", 42) # -- PARSE MISMATCH: self.assert_mismatch(parser, "Variant: __YES__") self.assert_mismatch(parser, "Variant: yes ") self.assert_mismatch(parser, "Variant: yes ZZZ") self.assert_mismatch(parser, "Variant: -1") # -- PERFORM TESTS: self.ensure_can_parse_all_enum_values(parser, parse_yesno, "Variant: %s", "variant") def test_make_variant__uncompiled(self): type_converters = [parse_yesno, parse_number] parse_variant1 = TypeBuilder.make_variant(type_converters) self.check_parse_variant_number_or_yesno(parse_variant1) def test_make_variant__compiled(self): # -- REVERSED ORDER VARIANT: type_converters = [parse_number, parse_yesno] parse_variant2 = TypeBuilder.make_variant(type_converters, compiled=True) self.check_parse_variant_number_or_yesno(parse_variant2) def test_make_variant__with_re_opts_0(self): # -- SKIP: IGNORECASE checks which would raise an error in strict mode. type_converters = [parse_number, parse_yesno] parse_variant3 = TypeBuilder.make_variant(type_converters, re_opts=0) self.check_parse_variant_number_or_yesno(parse_variant3, with_ignorecase=False) def test_make_variant__with_re_opts_IGNORECASE(self): type_converters = [parse_number, parse_yesno] parse_variant3 = TypeBuilder.make_variant(type_converters, re_opts=re.IGNORECASE) self.check_parse_variant_number_or_yesno(parse_variant3) def test_make_variant__with_strict(self): # -- SKIP: IGNORECASE checks which would raise an error in strict mode. type_converters = [parse_number, parse_yesno] parse_variant = TypeBuilder.make_variant(type_converters, strict=True) self.check_parse_variant_number_or_yesno(parse_variant, with_ignorecase=False) def test_make_variant__with_strict_raises_error_on_case_mismatch(self): # -- NEEDS: # * re_opts=0 (IGNORECASE disabled) # * strict=True, allow that an error is raised type_converters = [parse_number, parse_yesno] parse_variant = TypeBuilder.make_variant(type_converters, strict=True, re_opts=0) schema = "Variant: {variant:YesNo_or_Number}" parser = parse.Parser(schema, dict(YesNo_or_Number=parse_variant)) self.assertRaises(AssertionError, parser.parse, "Variant: YES") def test_make_variant__without_strict_may_return_none_on_case_mismatch(self): # -- NEEDS: # * re_opts=0 (IGNORECASE disabled) # * strict=False, otherwise an error is raised type_converters = [parse_number, parse_yesno] parse_variant = TypeBuilder.make_variant(type_converters, re_opts=0, strict=False) schema = "Variant: {variant:YesNo_or_Number}" parser = parse.Parser(schema, dict(YesNo_or_Number=parse_variant)) result = parser.parse("Variant: No") self.assertNotEqual(result, None) self.assertEqual(result["variant"], None) def test_make_variant__with_strict_and_compiled_raises_error_on_case_mismatch(self): # XXX re_opts=0 seems to work differently. # -- NEEDS: # * re_opts=0 (IGNORECASE disabled) # * strict=True, allow that an error is raised type_converters = [parse_number, parse_yesno] # -- ENSURE: coverage for cornercase. parse_number.matcher = re.compile(parse_number.pattern) parse_variant = TypeBuilder.make_variant(type_converters, compiled=True, re_opts=0, strict=True) schema = "Variant: {variant:YesNo_or_Number}" parser = parse.Parser(schema, dict(YesNo_or_Number=parse_variant)) # XXX self.assertRaises(AssertionError, parser.parse, "Variant: YES") result = parser.parse("Variant: Yes") self.assertNotEqual(result, None) self.assertEqual(result["variant"], True) def test_make_variant__without_strict_and_compiled_may_return_none_on_case_mismatch(self): # XXX re_opts=0 seems to work differently. # -- NEEDS: # * re_opts=0 (IGNORECASE disabled) # * strict=False, otherwise an error is raised type_converters = [parse_number, parse_yesno] parse_variant = TypeBuilder.make_variant(type_converters, compiled=True, re_opts=0, strict=True) schema = "Variant: {variant:YesNo_or_Number}" parser = parse.Parser(schema, dict(YesNo_or_Number=parse_variant)) result = parser.parse("Variant: NO") self.assertNotEqual(result, None) self.assertEqual(result["variant"], False) def test_make_variant__with_color_or_person(self): type_converters = [parse_color, parse_person_choice] parse_variant2 = TypeBuilder.make_variant(type_converters) schema = "Variant2: {variant:Color_or_Person}" parser = parse.Parser(schema, dict(Color_or_Person=parse_variant2)) # -- TYPE 1: Color self.assert_match(parser, "Variant2: red", "variant", Color.red) self.assert_match(parser, "Variant2: blue", "variant", Color.blue) # -- TYPE 2: Person self.assert_match(parser, "Variant2: Alice", "variant", "Alice") self.assert_match(parser, "Variant2: Bob", "variant", "Bob") self.assert_match(parser, "Variant2: Charly", "variant", "Charly") # -- PARSE MISMATCH: self.assert_mismatch(parser, "Variant2: __Alice__") self.assert_mismatch(parser, "Variant2: Alice ") self.assert_mismatch(parser, "Variant2: Alice2") self.assert_mismatch(parser, "Variant2: red2") # -- PERFORM TESTS: self.ensure_can_parse_all_enum_values(parser, parse_color, "Variant2: %s", "variant") self.ensure_can_parse_all_choices(parser, parse_person_choice, "Variant2: %s", "variant") class TestParserWithManyTypedFields(ParseTypeTestCase): parse_variant1 = TypeBuilder.make_variant([parse_number, parse_yesno]) parse_variant1.name = "Number_or_YesNo" parse_variant2 = TypeBuilder.make_variant([parse_color, parse_person_choice]) parse_variant2.name = "Color_or_PersonChoice" TYPE_CONVERTERS = [ parse_number, parse_yesno, parse_color, parse_person_choice, parse_variant1, parse_variant2, ] def test_parse_with_many_named_fields(self): type_dict = build_type_dict(self.TYPE_CONVERTERS) schema = """\ Number: {number:Number} YesNo: {answer:YesNo} Color: {color:Color} Person: {person:PersonChoice} Variant1: {variant1:Number_or_YesNo} Variant2: {variant2:Color_or_PersonChoice} """ parser = parse.Parser(schema, type_dict) text = """\ Number: 12 YesNo: yes Color: red Person: Alice Variant1: 42 Variant2: Bob """ expected = dict( number=12, answer=True, color=Color.red, person="Alice", variant1=42, variant2="Bob" ) result = parser.parse(text) self.assertIsNotNone(result) self.assertEqual(result.named, expected) def test_parse_with_many_unnamed_fields(self): type_dict = build_type_dict(self.TYPE_CONVERTERS) schema = """\ Number: {:Number} YesNo: {:YesNo} Color: {:Color} Person: {:PersonChoice} """ # -- OMIT: XFAIL, due to group_index delta counting => Parser problem. # Variant2: {:Color_or_PersonChoice} # Variant1: {:Number_or_YesNo} parser = parse.Parser(schema, type_dict) text = """\ Number: 12 YesNo: yes Color: red Person: Alice """ # SKIP: Variant2: Bob # SKIP: Variant1: 42 expected = [ 12, True, Color.red, "Alice", ] # -- SKIP: "Bob", 42 ] result = parser.parse(text) self.assertIsNotNone(result) self.assertEqual(result.fixed, tuple(expected)) def test_parse_with_many_unnamed_fields_with_variants(self): type_dict = build_type_dict(self.TYPE_CONVERTERS) schema = """\ Number: {:Number} YesNo: {:YesNo} Color: {:Color} Person: {:PersonChoice} Variant2: {:Color_or_PersonChoice} Variant1: {:Number_or_YesNo} """ # -- OMIT: XFAIL, due to group_index delta counting => Parser problem. parser = parse.Parser(schema, type_dict) text = """\ Number: 12 YesNo: yes Color: red Person: Alice Variant2: Bob Variant1: 42 """ expected = [ 12, True, Color.red, "Alice", "Bob", 42 ] result = parser.parse(text) self.assertIsNotNone(result) self.assertEqual(result.fixed, tuple(expected)) # ----------------------------------------------------------------------------- # MAIN: # ----------------------------------------------------------------------------- if __name__ == '__main__': unittest.main() # Copyright (c) 2012-2013 by Jens Engel (https://github/jenisys/parse_type) # # 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. parse_type-0.6.2/tests/test_cardinality.py000077500000000000000000000565411445072161600207740ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- """ Test suite to test the :mod:`parse_type.cardinality` module. """ from __future__ import absolute_import from .parse_type_test import ParseTypeTestCase, parse_number from parse_type import Cardinality, TypeBuilder, build_type_dict from parse import Parser import parse import unittest # ----------------------------------------------------------------------------- # TEST CASE: TestCardinality # ----------------------------------------------------------------------------- class TestCardinality(ParseTypeTestCase): def test_enum_basics(self): assert Cardinality.optional is Cardinality.zero_or_one assert Cardinality.many0 is Cardinality.zero_or_more assert Cardinality.many is Cardinality.one_or_more def check_pattern_for_cardinality_one(self, pattern, new_pattern): expected_pattern = Cardinality.one.make_pattern(pattern) self.assertEqual(pattern, new_pattern) self.assertEqual(new_pattern, expected_pattern) def check_pattern_for_cardinality_zero_or_one(self, pattern, new_pattern): expected_pattern = Cardinality.zero_or_one.schema % pattern self.assertNotEqual(pattern, new_pattern) self.assertEqual(new_pattern, expected_pattern) def check_pattern_for_cardinality_zero_or_more(self, pattern, new_pattern): expected_pattern = Cardinality.zero_or_more.make_pattern(pattern) self.assertNotEqual(pattern, new_pattern) self.assertEqual(new_pattern, expected_pattern) def check_pattern_for_cardinality_one_or_more(self, pattern, new_pattern): expected_pattern = Cardinality.one_or_more.make_pattern(pattern) self.assertNotEqual(pattern, new_pattern) self.assertEqual(new_pattern, expected_pattern) def check_pattern_for_cardinality_optional(self, pattern, new_pattern): expected = Cardinality.optional.make_pattern(pattern) self.assertEqual(new_pattern, expected) self.check_pattern_for_cardinality_zero_or_one(pattern, new_pattern) def check_pattern_for_cardinality_many0(self, pattern, new_pattern): expected = Cardinality.many0.make_pattern(pattern) self.assertEqual(new_pattern, expected) self.check_pattern_for_cardinality_zero_or_more(pattern, new_pattern) def check_pattern_for_cardinality_many(self, pattern, new_pattern): expected = Cardinality.many.make_pattern(pattern) self.assertEqual(new_pattern, expected) self.check_pattern_for_cardinality_one_or_more(pattern, new_pattern) def test_make_pattern(self): data = [ (Cardinality.one, r"\d+", r"\d+"), (Cardinality.one, r"\w+", None), (Cardinality.zero_or_one, r"\w+", None), (Cardinality.one_or_more, r"\w+", None), (Cardinality.optional, "XXX", Cardinality.zero_or_one.make_pattern("XXX")), (Cardinality.many0, "XXX", Cardinality.zero_or_more.make_pattern("XXX")), (Cardinality.many, "XXX", Cardinality.one_or_more.make_pattern("XXX")), ] for cardinality, pattern, expected_pattern in data: if expected_pattern is None: expected_pattern = cardinality.make_pattern(pattern) new_pattern = cardinality.make_pattern(pattern) self.assertEqual(new_pattern, expected_pattern) name = cardinality.name checker = getattr(self, "check_pattern_for_cardinality_%s" % name) checker(pattern, new_pattern) def test_make_pattern_for_zero_or_one(self): patterns = [r"\d", r"\d+", r"\w+", r"XXX" ] expecteds = [r"(\d)?", r"(\d+)?", r"(\w+)?", r"(XXX)?" ] for pattern, expected in zip(patterns, expecteds): new_pattern = Cardinality.zero_or_one.make_pattern(pattern) self.assertEqual(new_pattern, expected) self.check_pattern_for_cardinality_zero_or_one(pattern, new_pattern) def test_make_pattern_for_zero_or_more(self): pattern = "XXX" expected = r"(XXX)?(\s*,\s*(XXX))*" new_pattern = Cardinality.zero_or_more.make_pattern(pattern) self.assertEqual(new_pattern, expected) self.check_pattern_for_cardinality_zero_or_more(pattern, new_pattern) def test_make_pattern_for_one_or_more(self): pattern = "XXX" expected = r"(XXX)(\s*,\s*(XXX))*" new_pattern = Cardinality.one_or_more.make_pattern(pattern) self.assertEqual(new_pattern, expected) self.check_pattern_for_cardinality_one_or_more(pattern, new_pattern) def test_is_many(self): is_many_true_valueset = set( [Cardinality.zero_or_more, Cardinality.one_or_more]) for cardinality in Cardinality: expected = cardinality in is_many_true_valueset self.assertEqual(cardinality.is_many(), expected) # ----------------------------------------------------------------------------- # TEST CASE: CardinalityTypeBuilderTest # ----------------------------------------------------------------------------- class CardinalityTypeBuilderTest(ParseTypeTestCase): def check_parse_number_with_zero_or_one(self, parse_candidate, type_name="OptionalNumber"): schema = "Optional: {number:%s}" % type_name type_dict = { "Number": parse_number, type_name: parse_candidate, } parser = parse.Parser(schema, type_dict) # -- PERFORM TESTS: self.assert_match(parser, "Optional: ", "number", None) self.assert_match(parser, "Optional: 1", "number", 1) self.assert_match(parser, "Optional: 42", "number", 42) # -- PARSE MISMATCH: self.assert_mismatch(parser, "Optional: x", "number") # Not a Number. self.assert_mismatch(parser, "Optional: -1", "number") # Negative. self.assert_mismatch(parser, "Optional: a, b", "number") # List of ... def check_parse_number_with_optional(self, parse_candidate, type_name="OptionalNumber"): self.check_parse_number_with_zero_or_one(parse_candidate, type_name) def check_parse_number_with_zero_or_more(self, parse_candidate, type_name="Numbers0"): schema = "List: {numbers:%s}" % type_name type_dict = { type_name: parse_candidate, } parser = parse.Parser(schema, type_dict) # -- PERFORM TESTS: self.assert_match(parser, "List: ", "numbers", [ ]) self.assert_match(parser, "List: 1", "numbers", [ 1 ]) self.assert_match(parser, "List: 1, 2", "numbers", [ 1, 2 ]) self.assert_match(parser, "List: 1, 2, 3", "numbers", [ 1, 2, 3 ]) # -- PARSE MISMATCH: self.assert_mismatch(parser, "List: x", "numbers") # Not a Number. self.assert_mismatch(parser, "List: -1", "numbers") # Negative. self.assert_mismatch(parser, "List: 1,", "numbers") # Trailing sep. self.assert_mismatch(parser, "List: a, b", "numbers") # List of ... def check_parse_number_with_one_or_more(self, parse_candidate, type_name="Numbers"): schema = "List: {numbers:%s}" % type_name type_dict = { "Number": parse_number, type_name: parse_candidate, } parser = parse.Parser(schema, type_dict) # -- PERFORM TESTS: self.assert_match(parser, "List: 1", "numbers", [ 1 ]) self.assert_match(parser, "List: 1, 2", "numbers", [ 1, 2 ]) self.assert_match(parser, "List: 1, 2, 3", "numbers", [ 1, 2, 3 ]) # -- PARSE MISMATCH: self.assert_mismatch(parser, "List: ", "numbers") # Zero items. self.assert_mismatch(parser, "List: x", "numbers") # Not a Number. self.assert_mismatch(parser, "List: -1", "numbers") # Negative. self.assert_mismatch(parser, "List: 1,", "numbers") # Trailing sep. self.assert_mismatch(parser, "List: a, b", "numbers") # List of ... def check_parse_choice_with_optional(self, parse_candidate): # Choice (["red", "green", "blue"]) schema = "Optional: {color:OptionalChoiceColor}" parser = parse.Parser(schema, dict(OptionalChoiceColor=parse_candidate)) # -- PERFORM TESTS: self.assert_match(parser, "Optional: ", "color", None) self.assert_match(parser, "Optional: red", "color", "red") self.assert_match(parser, "Optional: green", "color", "green") self.assert_match(parser, "Optional: blue", "color", "blue") # -- PARSE MISMATCH: self.assert_mismatch(parser, "Optional: r", "color") # Not a Color. self.assert_mismatch(parser, "Optional: redx", "color") # Similar. self.assert_mismatch(parser, "Optional: red, blue", "color") # List of ... def check_parse_number_with_many(self, parse_candidate, type_name="Numbers"): self.check_parse_number_with_one_or_more(parse_candidate, type_name) def check_parse_number_with_many0(self, parse_candidate, type_name="Numbers0"): self.check_parse_number_with_zero_or_more(parse_candidate, type_name) # ----------------------------------------------------------------------------- # TEST CASE: TestTypeBuilder4Cardinality # ----------------------------------------------------------------------------- class TestTypeBuilder4Cardinality(CardinalityTypeBuilderTest): def test_with_zero_or_one_basics(self): parse_opt_number = TypeBuilder.with_zero_or_one(parse_number) self.assertEqual(parse_opt_number.pattern, r"(\d+)?") def test_with_zero_or_one__number(self): parse_opt_number = TypeBuilder.with_zero_or_one(parse_number) self.check_parse_number_with_zero_or_one(parse_opt_number) def test_with_optional__number(self): # -- ALIAS FOR: zero_or_one parse_opt_number = TypeBuilder.with_optional(parse_number) self.check_parse_number_with_optional(parse_opt_number) def test_with_optional__choice(self): # -- ALIAS FOR: zero_or_one parse_color = TypeBuilder.make_choice(["red", "green", "blue"]) parse_opt_color = TypeBuilder.with_optional(parse_color) self.check_parse_choice_with_optional(parse_opt_color) def test_with_zero_or_more_basics(self): parse_numbers = TypeBuilder.with_zero_or_more(parse_number) self.assertEqual(parse_numbers.pattern, r"(\d+)?(\s*,\s*(\d+))*") def test_with_zero_or_more__number(self): parse_numbers = TypeBuilder.with_zero_or_more(parse_number) self.check_parse_number_with_zero_or_more(parse_numbers) def test_with_zero_or_more__choice(self): parse_color = TypeBuilder.make_choice(["red", "green", "blue"]) parse_colors = TypeBuilder.with_zero_or_more(parse_color) parse_colors.name = "Colors0" extra_types = build_type_dict([ parse_colors ]) schema = "List: {colors:Colors0}" parser = parse.Parser(schema, extra_types) # -- PERFORM TESTS: self.assert_match(parser, "List: ", "colors", [ ]) self.assert_match(parser, "List: green", "colors", [ "green" ]) self.assert_match(parser, "List: red, green", "colors", [ "red", "green" ]) # -- PARSE MISMATCH: self.assert_mismatch(parser, "List: x", "colors") # Not a Color. self.assert_mismatch(parser, "List: black", "colors") # Unknown self.assert_mismatch(parser, "List: red,", "colors") # Trailing sep. self.assert_mismatch(parser, "List: a, b", "colors") # List of ... def test_with_one_or_more_basics(self): parse_numbers = TypeBuilder.with_one_or_more(parse_number) self.assertEqual(parse_numbers.pattern, r"(\d+)(\s*,\s*(\d+))*") def test_with_one_or_more_basics_with_other_separator(self): parse_numbers2 = TypeBuilder.with_one_or_more(parse_number, listsep=';') self.assertEqual(parse_numbers2.pattern, r"(\d+)(\s*;\s*(\d+))*") parse_numbers2 = TypeBuilder.with_one_or_more(parse_number, listsep=':') self.assertEqual(parse_numbers2.pattern, r"(\d+)(\s*:\s*(\d+))*") def test_with_one_or_more(self): parse_numbers = TypeBuilder.with_one_or_more(parse_number) self.check_parse_number_with_one_or_more(parse_numbers) def test_with_many(self): # -- ALIAS FOR: one_or_more parse_numbers = TypeBuilder.with_many(parse_number) self.check_parse_number_with_many(parse_numbers) def test_with_many0(self): # -- ALIAS FOR: one_or_more parse_numbers = TypeBuilder.with_many0(parse_number) self.check_parse_number_with_many0(parse_numbers) def test_with_one_or_more_choice(self): parse_color = TypeBuilder.make_choice(["red", "green", "blue"]) parse_colors = TypeBuilder.with_one_or_more(parse_color) parse_colors.name = "Colors" extra_types = build_type_dict([ parse_colors ]) schema = "List: {colors:Colors}" parser = parse.Parser(schema, extra_types) # -- PERFORM TESTS: self.assert_match(parser, "List: green", "colors", [ "green" ]) self.assert_match(parser, "List: red, green", "colors", [ "red", "green" ]) # -- PARSE MISMATCH: self.assert_mismatch(parser, "List: ", "colors") # Zero items. self.assert_mismatch(parser, "List: x", "colors") # Not a Color. self.assert_mismatch(parser, "List: black", "colors") # Unknown self.assert_mismatch(parser, "List: red,", "colors") # Trailing sep. self.assert_mismatch(parser, "List: a, b", "colors") # List of ... def test_with_one_or_more_enum(self): parse_color = TypeBuilder.make_enum({"red": 1, "green":2, "blue": 3}) parse_colors = TypeBuilder.with_one_or_more(parse_color) parse_colors.name = "Colors" extra_types = build_type_dict([ parse_colors ]) schema = "List: {colors:Colors}" parser = parse.Parser(schema, extra_types) # -- PERFORM TESTS: self.assert_match(parser, "List: green", "colors", [ 2 ]) self.assert_match(parser, "List: red, green", "colors", [ 1, 2 ]) # -- PARSE MISMATCH: self.assert_mismatch(parser, "List: ", "colors") # Zero items. self.assert_mismatch(parser, "List: x", "colors") # Not a Color. self.assert_mismatch(parser, "List: black", "colors") # Unknown self.assert_mismatch(parser, "List: red,", "colors") # Trailing sep. self.assert_mismatch(parser, "List: a, b", "colors") # List of ... def test_with_one_or_more_with_other_separator(self): parse_numbers2 = TypeBuilder.with_one_or_more(parse_number, listsep=';') parse_numbers2.name = "Numbers2" extra_types = build_type_dict([ parse_numbers2 ]) schema = "List: {numbers:Numbers2}" parser = parse.Parser(schema, extra_types) # -- PERFORM TESTS: self.assert_match(parser, "List: 1", "numbers", [ 1 ]) self.assert_match(parser, "List: 1; 2", "numbers", [ 1, 2 ]) self.assert_match(parser, "List: 1; 2; 3", "numbers", [ 1, 2, 3 ]) def test_with_cardinality_one(self): parse_number2 = TypeBuilder.with_cardinality(Cardinality.one, parse_number) assert parse_number2 is parse_number def test_with_cardinality_zero_or_one(self): parse_opt_number = TypeBuilder.with_cardinality( Cardinality.zero_or_one, parse_number) self.check_parse_number_with_zero_or_one(parse_opt_number) def test_with_cardinality_zero_or_more(self): parse_many0_numbers = TypeBuilder.with_cardinality( Cardinality.zero_or_more, parse_number) self.check_parse_number_with_zero_or_more(parse_many0_numbers) def test_with_cardinality_one_or_more(self): parse_many_numbers = TypeBuilder.with_cardinality( Cardinality.one_or_more, parse_number) self.check_parse_number_with_one_or_more(parse_many_numbers) def test_with_cardinality_optional(self): parse_opt_number = TypeBuilder.with_cardinality( Cardinality.optional, parse_number) self.check_parse_number_with_optional(parse_opt_number) def test_with_cardinality_many0(self): parse_many0_numbers = TypeBuilder.with_cardinality( Cardinality.many0, parse_number) self.check_parse_number_with_zero_or_more(parse_many0_numbers) def test_with_cardinality_many(self): parse_many_numbers = TypeBuilder.with_cardinality( Cardinality.many, parse_number) self.check_parse_number_with_many(parse_many_numbers) def test_parse_with_optional_and_named_fields(self): parse_opt_number = TypeBuilder.with_optional(parse_number) parse_opt_number.name = "Number?" type_dict = build_type_dict([parse_opt_number, parse_number]) schema = "Numbers: {number1:Number?} {number2:Number}" parser = parse.Parser(schema, type_dict) # -- CASE: Optional number is present result = parser.parse("Numbers: 34 12") expected = dict(number1=34, number2=12) self.assertIsNotNone(result) self.assertEqual(result.named, expected) # -- CASE: Optional number is missing result = parser.parse("Numbers: 12") expected = dict(number1=None, number2=12) self.assertIsNotNone(result) self.assertEqual(result.named, expected) def test_parse_with_optional_and_unnamed_fields(self): # -- ENSURE: Cardinality.optional.group_count is correct # REQUIRES: Parser := parse_type.Parser with group_count support parse_opt_number = TypeBuilder.with_optional(parse_number) parse_opt_number.name = "Number?" type_dict = build_type_dict([parse_opt_number, parse_number]) schema = "Numbers: {:Number?} {:Number}" parser = Parser(schema, type_dict) # -- CASE: Optional number is present result = parser.parse("Numbers: 34 12") expected = (34, 12) self.assertIsNotNone(result) self.assertEqual(result.fixed, tuple(expected)) # -- CASE: Optional number is missing result = parser.parse("Numbers: 12") expected = (None, 12) self.assertIsNotNone(result) self.assertEqual(result.fixed, tuple(expected)) def test_parse_with_many_and_unnamed_fields(self): # -- ENSURE: Cardinality.one_or_more.group_count is correct # REQUIRES: Parser := parse_type.Parser with group_count support parse_many_numbers = TypeBuilder.with_many(parse_number) parse_many_numbers.name = "Number+" type_dict = build_type_dict([parse_many_numbers, parse_number]) schema = "Numbers: {:Number+} {:Number}" parser = Parser(schema, type_dict) # -- CASE: result = parser.parse("Numbers: 1, 2, 3 42") expected = ([1, 2, 3], 42) self.assertIsNotNone(result) self.assertEqual(result.fixed, tuple(expected)) result = parser.parse("Numbers: 3 43") expected = ([ 3 ], 43) self.assertIsNotNone(result) self.assertEqual(result.fixed, tuple(expected)) def test_parse_with_many0_and_unnamed_fields(self): # -- ENSURE: Cardinality.zero_or_more.group_count is correct # REQUIRES: Parser := parse_type.Parser with group_count support parse_many0_numbers = TypeBuilder.with_many0(parse_number) parse_many0_numbers.name = "Number*" type_dict = build_type_dict([parse_many0_numbers, parse_number]) schema = "Numbers: {:Number*} {:Number}" parser = Parser(schema, type_dict) # -- CASE: Optional numbers are present result = parser.parse("Numbers: 1, 2, 3 42") expected = ([1, 2, 3], 42) self.assertIsNotNone(result) self.assertEqual(result.fixed, tuple(expected)) # -- CASE: Optional numbers are missing := EMPTY-LIST result = parser.parse("Numbers: 43") expected = ([ ], 43) self.assertIsNotNone(result) self.assertEqual(result.fixed, tuple(expected)) # class TestParserWithManyTypedFields(ParseTypeTestCase): #parse_variant1 = TypeBuilder.make_variant([parse_number, parse_yesno]) #parse_variant1.name = "Number_or_YesNo" #parse_variant2 = TypeBuilder.make_variant([parse_color, parse_person_choice]) #parse_variant2.name = "Color_or_PersonChoice" #TYPE_CONVERTERS = [ # parse_number, # parse_yesno, # parse_color, # parse_person_choice, # parse_variant1, # parse_variant2, #] # # def test_parse_with_many_named_fields(self): # type_dict = build_type_dict(self.TYPE_CONVERTERS) # schema = """\ #Number: {number:Number} #YesNo: {answer:YesNo} #Color: {color:Color} #Person: {person:PersonChoice} #Variant1: {variant1:Number_or_YesNo} #Variant2: {variant2:Color_or_PersonChoice} #""" # parser = parse.Parser(schema, type_dict) # # text = """\ #Number: 12 #YesNo: yes #Color: red #Person: Alice #Variant1: 42 #Variant2: Bob #""" # expected = dict( # number=12, # answer=True, # color=Color.red, # person="Alice", # variant1=42, # variant2="Bob" # ) # # result = parser.parse(text) # self.assertIsNotNone(result) # self.assertEqual(result.named, expected) # def test_parse_with_many_unnamed_fields(self): # type_dict = build_type_dict(self.TYPE_CONVERTERS) # schema = """\ #Number: {:Number} #YesNo: {:YesNo} #Color: {:Color} #Person: {:PersonChoice} #""" # # -- OMIT: XFAIL, due to group_index delta counting => Parser problem. # # Variant2: {:Color_or_PersonChoice} # # Variant1: {:Number_or_YesNo} # parser = parse.Parser(schema, type_dict) # # text = """\ #Number: 12 #YesNo: yes #Color: red #Person: Alice #""" # # SKIP: Variant2: Bob # # SKIP: Variant1: 42 # expected = [ 12, True, Color.red, "Alice", ] # -- SKIP: "Bob", 42 ] # # result = parser.parse(text) # self.assertIsNotNone(result) # self.assertEqual(result.fixed, tuple(expected)) # ----------------------------------------------------------------------------- # MAIN: # ----------------------------------------------------------------------------- if __name__ == '__main__': unittest.main() # Copyright (c) 2012-2013 by Jens Engel (https://github/jenisys/parse_type) # # 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. parse_type-0.6.2/tests/test_cardinality_field.py000077500000000000000000000376361445072161600221430ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- """ Test experiment for parse. Add cardinality format field after type: "... {person:Person?} ..." -- CARDINALITY: Zero or one, 0..1 (optional) "... {persons:Person*} ..." -- CARDINALITY: Zero or more, 0..N (many0) "... {persons:Person+} ..." -- CARDINALITY: One or more, 1..N (many) REQUIRES: parse >= 1.5.3.1 ('pattern' attribute support and further extensions) STATUS: IDEA, working prototype with patched parse module, but not accepted. """ from __future__ import absolute_import from .parse_type_test \ import TestCase, parse_number, unittest from .test_cardinality import CardinalityTypeBuilderTest from parse_type import Cardinality from parse_type.cardinality_field \ import CardinalityField, CardinalityFieldTypeBuilder, MissingTypeError # ------------------------------------------------------------------------- # TEST CASE: TestParseTypeWithCardinalityField # ------------------------------------------------------------------------- class TestCardinalityField(TestCase): VALID_TYPE_NAMES = ["Number?", "Number*", "Number+"] INVALID_TYPE_NAMES = ["?Invalid", "Inval*d", "In+valid"] def test_pattern_chars(self): for pattern_char in CardinalityField.pattern_chars: self.assertIn(pattern_char, CardinalityField.from_char_map) def test_to_from_char_map_symmetry(self): for cardinality, char in CardinalityField.to_char_map.items(): self.assertEqual(cardinality, CardinalityField.from_char_map[char]) for char, cardinality in CardinalityField.from_char_map.items(): self.assertEqual(char, CardinalityField.to_char_map[cardinality]) def test_matches_type_name(self): for type_name in self.VALID_TYPE_NAMES: self.assertTrue(CardinalityField.matches_type(type_name)) for type_name in self.INVALID_TYPE_NAMES: self.assertFalse(CardinalityField.matches_type(type_name)) def test_split_type__with_valid_special_names(self): actual = CardinalityField.split_type("Color?") self.assertEqual(actual, ("Color", Cardinality.optional)) self.assertEqual(actual, ("Color", Cardinality.zero_or_one)) actual = CardinalityField.split_type("Color+") self.assertEqual(actual, ("Color", Cardinality.many)) self.assertEqual(actual, ("Color", Cardinality.one_or_more)) actual = CardinalityField.split_type("Color*") self.assertEqual(actual, ("Color", Cardinality.many0)) self.assertEqual(actual, ("Color", Cardinality.zero_or_more)) def test_split_type__with_valid_special_names2(self): for type_name in self.VALID_TYPE_NAMES: self.assertTrue(CardinalityField.matches_type(type_name)) cardinality_char = type_name[-1] expected_basename = type_name[:-1] expected_cardinality = CardinalityField.from_char_map[cardinality_char] expected = (expected_basename, expected_cardinality) actual = CardinalityField.split_type(type_name) self.assertEqual(actual, expected) def test_split_type__with_cardinality_one(self): actual = CardinalityField.split_type("Color") self.assertEqual(actual, ("Color", Cardinality.one)) def test_split_type__with_invalid_names(self): for type_name in self.INVALID_TYPE_NAMES: expected = (type_name, Cardinality.one) actual = CardinalityField.split_type(type_name) self.assertEqual(actual, expected) self.assertFalse(CardinalityField.matches_type(type_name)) def test_make_type__with_cardinality_one(self): expected = "Number" type_name = CardinalityField.make_type("Number", Cardinality.one) self.assertEqual(type_name, expected) self.assertFalse(CardinalityField.matches_type(type_name)) def test_make_type__with_cardinality_optional(self): expected = "Number?" type_name = CardinalityField.make_type("Number", Cardinality.optional) self.assertEqual(type_name, expected) self.assertTrue(CardinalityField.matches_type(type_name)) type_name2 = CardinalityField.make_type("Number", Cardinality.zero_or_one) self.assertEqual(type_name2, expected) self.assertEqual(type_name2, type_name) def test_make_type__with_cardinality_many(self): expected = "Number+" type_name = CardinalityField.make_type("Number", Cardinality.many) self.assertEqual(type_name, expected) self.assertTrue(CardinalityField.matches_type(type_name)) type_name2 = CardinalityField.make_type("Number", Cardinality.one_or_more) self.assertEqual(type_name2, expected) self.assertEqual(type_name2, type_name) def test_make_type__with_cardinality_many0(self): expected = "Number*" type_name = CardinalityField.make_type("Number", Cardinality.many0) self.assertEqual(type_name, expected) self.assertTrue(CardinalityField.matches_type(type_name)) type_name2 = CardinalityField.make_type("Number", Cardinality.zero_or_more) self.assertEqual(type_name2, expected) self.assertEqual(type_name2, type_name) def test_split_type2make_type__symmetry_with_valid_names(self): for type_name in self.VALID_TYPE_NAMES: primary_name, cardinality = CardinalityField.split_type(type_name) type_name2 = CardinalityField.make_type(primary_name, cardinality) self.assertEqual(type_name, type_name2) def test_split_type2make_type__symmetry_with_cardinality_one(self): for type_name in self.INVALID_TYPE_NAMES: primary_name, cardinality = CardinalityField.split_type(type_name) type_name2 = CardinalityField.make_type(primary_name, cardinality) self.assertEqual(type_name, primary_name) self.assertEqual(type_name, type_name2) self.assertEqual(cardinality, Cardinality.one) # ------------------------------------------------------------------------- # TEST CASE: # ------------------------------------------------------------------------- class TestCardinalityFieldTypeBuilder(CardinalityTypeBuilderTest): INVALID_TYPE_DICT_DATA = [ (dict(), "empty type_dict"), (dict(NumberX=parse_number), "non-empty type_dict (wrong name)"), ] # -- UTILITY METHODS: def generate_type_variants(self,type_name): for pattern_char in CardinalityField.pattern_chars: special_name = "%s%s" % (type_name.strip(), pattern_char) self.assertTrue(CardinalityField.matches_type(special_name)) yield special_name # -- METHOD: CardinalityFieldTypeBuilder.create_type_variant() def test_create_type_variant__with_many_and_type_converter(self): type_builder = CardinalityFieldTypeBuilder parse_candidate = type_builder.create_type_variant("Number+", type_converter=parse_number) self.check_parse_number_with_many(parse_candidate, "Number+") def test_create_type_variant__with_optional_and_type_dict(self): type_builder = CardinalityFieldTypeBuilder parse_candidate = type_builder.create_type_variant("Number?", dict(Number=parse_number)) self.check_parse_number_with_optional(parse_candidate, "Number?") def test_create_type_variant__with_many_and_type_dict(self): type_builder = CardinalityFieldTypeBuilder parse_candidate = type_builder.create_type_variant("Number+", dict(Number=parse_number)) self.check_parse_number_with_many(parse_candidate, "Number+") def test_create_type_variant__with_many0_and_type_dict(self): type_builder = CardinalityFieldTypeBuilder parse_candidate = type_builder.create_type_variant("Number*", dict(Number=parse_number)) self.check_parse_number_with_many0(parse_candidate, "Number*") def test_create_type_variant__can_create_all_variants(self): type_builder = CardinalityFieldTypeBuilder for special_name in self.generate_type_variants("Number"): # -- CASE: type_converter parse_candidate = type_builder.create_type_variant(special_name, parse_number) self.assertTrue(callable(parse_candidate)) # -- CASE: type_dict parse_candidate = type_builder.create_type_variant(special_name, dict(Number=parse_number)) self.assertTrue(callable(parse_candidate)) def test_create_type_variant__raises_error_with_invalid_type_name(self): type_builder = CardinalityFieldTypeBuilder for invalid_type_name in TestCardinalityField.INVALID_TYPE_NAMES: with self.assertRaises(ValueError): type_builder.create_type_variant(invalid_type_name, parse_number) def test_create_type_variant__raises_error_with_missing_primary_type(self): type_builder = CardinalityFieldTypeBuilder for special_name in self.generate_type_variants("Number"): for type_dict, description in self.INVALID_TYPE_DICT_DATA: with self.assertRaises(MissingTypeError): type_builder.create_type_variant(special_name, type_dict) # -- METHOD: CardinalityFieldTypeBuilder.create_type_variants() def test_create_type_variants__all(self): type_builder = CardinalityFieldTypeBuilder special_names = ["Number?", "Number+", "Number*"] type_dict = dict(Number=parse_number) new_types = type_builder.create_type_variants(special_names, type_dict) self.assertSequenceEqual(set(new_types.keys()), set(special_names)) self.assertEqual(len(new_types), 3) parse_candidate = new_types["Number?"] self.check_parse_number_with_optional(parse_candidate, "Number?") parse_candidate = new_types["Number+"] self.check_parse_number_with_many(parse_candidate, "Number+") parse_candidate = new_types["Number*"] self.check_parse_number_with_many0(parse_candidate, "Number*") def test_create_type_variants__raises_error_with_invalid_type_name(self): type_builder = CardinalityFieldTypeBuilder for invalid_type_name in TestCardinalityField.INVALID_TYPE_NAMES: type_dict = dict(Number=parse_number) with self.assertRaises(ValueError): type_names = [invalid_type_name] type_builder.create_type_variants(type_names, type_dict) def test_create_missing_type_variants__raises_error_with_missing_primary_type(self): type_builder = CardinalityFieldTypeBuilder for special_name in self.generate_type_variants("Number"): for type_dict, description in self.INVALID_TYPE_DICT_DATA: self.assertNotIn("Number", type_dict) with self.assertRaises(MissingTypeError): names = [special_name] type_builder.create_type_variants(names, type_dict) # -- METHOD: CardinalityFieldTypeBuilder.create_missing_type_variants() def test_create_missing_type_variants__all_missing(self): type_builder = CardinalityFieldTypeBuilder missing_names = ["Number?", "Number+", "Number*"] new_types = type_builder.create_missing_type_variants(missing_names, dict(Number=parse_number)) self.assertSequenceEqual(set(new_types.keys()), set(missing_names)) self.assertEqual(len(new_types), 3) def test_create_missing_type_variants__none_missing(self): # -- PREPARE: Create all types and store them in the type_dict. type_builder = CardinalityFieldTypeBuilder type_names = ["Number?", "Number+", "Number*"] all_type_names = ["Number", "Number?", "Number+", "Number*"] type_dict = dict(Number=parse_number) new_types = type_builder.create_missing_type_variants(type_names, type_dict) type_dict.update(new_types) self.assertSequenceEqual(set(new_types.keys()), set(type_names)) self.assertSequenceEqual(set(type_dict.keys()), set(all_type_names)) # -- TEST: All special types are already stored in the type_dict. new_types2 = type_builder.create_missing_type_variants(type_names, type_dict) self.assertEqual(len(new_types2), 0) def test_create_missing_type_variants__some_missing(self): # -- PREPARE: Create some types and store them in the type_dict. type_builder = CardinalityFieldTypeBuilder special_names = ["Number?", "Number+", "Number*"] type_names1 = ["Number?", "Number*"] type_names2 = special_names type_dict = dict(Number=parse_number) new_types = type_builder.create_missing_type_variants(type_names1, type_dict) type_dict.update(new_types) self.assertSequenceEqual(set(new_types.keys()), set(type_names1)) self.assertSequenceEqual(set(type_dict.keys()), set(["Number", "Number?", "Number*"])) # -- TEST: All special types are already stored in the type_dict. new_types2 = type_builder.create_missing_type_variants(type_names2, type_dict) self.assertEqual(len(new_types2), 1) self.assertSequenceEqual(set(new_types2.keys()), set(["Number+"])) def test_create_type_variant__raises_error_with_invalid_type_name(self): type_builder = CardinalityFieldTypeBuilder for invalid_type_name in TestCardinalityField.INVALID_TYPE_NAMES: type_dict = dict(Number=parse_number) with self.assertRaises(ValueError): type_names = [invalid_type_name] type_builder.create_missing_type_variants(type_names, type_dict) def test_create_missing_type_variants__raises_error_with_missing_primary_type(self): type_builder = CardinalityFieldTypeBuilder for special_name in self.generate_type_variants("Number"): for type_dict, description in self.INVALID_TYPE_DICT_DATA: self.assertNotIn("Number", type_dict) with self.assertRaises(MissingTypeError): names = [special_name] type_builder.create_missing_type_variants(names, type_dict) # ----------------------------------------------------------------------------- # MAIN: # ----------------------------------------------------------------------------- if __name__ == '__main__': unittest.main() # Copyright (c) 2012-2013 by Jens Engel (https://github/jenisys/parse_type) # # 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. parse_type-0.6.2/tests/test_cardinality_field0.py000077500000000000000000000156221445072161600222120ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- """ Test experiment for parse. Add cardinality format field after type: "... {person:Person?} ..." -- CARDINALITY: Zero or one, 0..1 (optional) "... {persons:Person*} ..." -- CARDINALITY: Zero or more, 0..N (many0) "... {persons:Person+} ..." -- CARDINALITY: One or more, 1..N (many) REQUIRES: parse >= 1.5.3.1 ('pattern' attribute support and further extensions) STATUS: IDEA, working prototype with patched parse module, but not accepted. """ from __future__ import absolute_import from .parse_type_test import ParseTypeTestCase from parse_type import TypeBuilder, build_type_dict import parse import unittest ENABLED = False if ENABLED: # ------------------------------------------------------------------------- # TEST CASE: TestParseTypeWithCardinalityField # ------------------------------------------------------------------------- class TestParseTypeWithCardinalityField(ParseTypeTestCase): """ Test cardinality field part in parse type expressions, ala: "... {person:Person?} ..." -- OPTIONAL: cardinality is zero or one. "... {persons:Person*} ..." -- MANY0: cardinality is zero or more. "... {persons:Person+} ..." -- MANY: cardinality is one or more. NOTE: * TypeBuilder has a similar and slightly more flexible feature. * Cardinality field part works currently only for user-defined types. """ def test_without_cardinality_field(self): # -- IMPLCIT CARDINALITY: one # -- SETUP: parse_person = TypeBuilder.make_choice(["Alice", "Bob", "Charly"]) parse_person.name = "Person" # For testing only. extra_types = build_type_dict([ parse_person ]) schema = "One: {person:Person}" parser = parse.Parser(schema, extra_types) # -- PERFORM TESTS: self.assert_match(parser, "One: Alice", "person", "Alice") self.assert_match(parser, "One: Bob", "person", "Bob") # -- PARSE MISMATCH: self.assert_mismatch(parser, "One: ", "person") # Missing. self.assert_mismatch(parser, "One: BAlice", "person") # Similar1. self.assert_mismatch(parser, "One: Boby", "person") # Similar2. self.assert_mismatch(parser, "One: a", "person") # INVALID ... def test_cardinality_field_with_zero_or_one(self): # -- SETUP: parse_person = TypeBuilder.make_choice(["Alice", "Bob", "Charly"]) parse_person.name = "Person" # For testing only. extra_types = build_type_dict([ parse_person ]) schema = "Optional: {person:Person?}" parser = parse.Parser(schema, extra_types) # -- PERFORM TESTS: self.assert_match(parser, "Optional: ", "person", None) self.assert_match(parser, "Optional: Alice", "person", "Alice") self.assert_match(parser, "Optional: Bob", "person", "Bob") # -- PARSE MISMATCH: self.assert_mismatch(parser, "Optional: Anna", "person") # Similar1. self.assert_mismatch(parser, "Optional: Boby", "person") # Similar2. self.assert_mismatch(parser, "Optional: a", "person") # INVALID ... def test_cardinality_field_with_one_or_more(self): # -- SETUP: parse_person = TypeBuilder.make_choice(["Alice", "Bob", "Charly"]) parse_person.name = "Person" # For testing only. extra_types = build_type_dict([ parse_person ]) schema = "List: {persons:Person+}" parser = parse.Parser(schema, extra_types) # -- PERFORM TESTS: self.assert_match(parser, "List: Alice", "persons", [ "Alice" ]) self.assert_match(parser, "List: Bob", "persons", [ "Bob" ]) self.assert_match(parser, "List: Bob, Alice", "persons", [ "Bob", "Alice" ]) # -- PARSE MISMATCH: self.assert_mismatch(parser, "List: ", "persons") # Zero items. self.assert_mismatch(parser, "List: BAlice", "persons") # Unknown1. self.assert_mismatch(parser, "List: Boby", "persons") # Unknown2. self.assert_mismatch(parser, "List: Alice,", "persons") # Trailing, self.assert_mismatch(parser, "List: a, b", "persons") # List of... def test_cardinality_field_with_zero_or_more(self): # -- SETUP: parse_person = TypeBuilder.make_choice(["Alice", "Bob", "Charly"]) parse_person.name = "Person" # For testing only. extra_types = build_type_dict([ parse_person ]) schema = "List: {persons:Person*}" parser = parse.Parser(schema, extra_types) # -- PERFORM TESTS: self.assert_match(parser, "List: ", "persons", [ ]) self.assert_match(parser, "List: Alice", "persons", [ "Alice" ]) self.assert_match(parser, "List: Bob", "persons", [ "Bob" ]) self.assert_match(parser, "List: Bob, Alice", "persons", [ "Bob", "Alice" ]) # -- PARSE MISMATCH: self.assert_mismatch(parser, "List:", "persons") # Too short. self.assert_mismatch(parser, "List: BAlice", "persons") # Unknown1. self.assert_mismatch(parser, "List: Boby", "persons") # Unknown2. self.assert_mismatch(parser, "List: Alice,", "persons") # Trailing, self.assert_mismatch(parser, "List: a, b", "persons") # List of... # ----------------------------------------------------------------------------- # MAIN: # ----------------------------------------------------------------------------- if __name__ == '__main__': unittest.main() # Copyright (c) 2012-2013 by Jens Engel (https://github/jenisys/parse_type) # # 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. parse_type-0.6.2/tests/test_cfparse.py000066400000000000000000000205451445072161600201040ustar00rootroot00000000000000# -*- coding: utf-8 -*- #!/usr/bin/env python # -*- coding: utf-8 -*- """ Test suite to test the :mod:`parse_type.cfparse` module. """ from __future__ import absolute_import from .parse_type_test import ParseTypeTestCase, parse_number, unittest from parse_type.cfparse import Parser from parse_type.cardinality_field \ import MissingTypeError, CardinalityFieldTypeBuilder # ----------------------------------------------------------------------------- # TEST CASE: # ----------------------------------------------------------------------------- class TestParser(ParseTypeTestCase): """ Test :class:`parse_type.cfparse.Parser`. Ensure that: * parser can parse fields with CardinalityField part even when these special type variants are not provided. * parser creates missing type converter variants for CardinalityFields as long as the primary type converter for cardinality=1 is provided. """ SPECIAL_FIELD_TYPES_DATA = [ ("{number1:Number?}", ["Number?"]), ("{number2:Number+}", ["Number+"]), ("{number3:Number*}", ["Number*"]), ("{number1:Number?} {number2:Number+} {number3:Number*}", ["Number?", "Number+", "Number*"]), ] def test_parser__can_parse_normal_fields(self): existing_types = dict(Number=parse_number) schema = "Number: {number:Number}" parser = Parser(schema, existing_types) self.assert_match(parser, "Number: 42", "number", 42) self.assert_match(parser, "Number: 123", "number", 123) self.assert_mismatch(parser, "Number: ") self.assert_mismatch(parser, "Number: XXX") self.assert_mismatch(parser, "Number: -123") def test_parser__can_parse_cardinality_field_optional(self): # -- CARDINALITY: 0..1 = zero_or_one = optional existing_types = dict(Number=parse_number) self.assertFalse("Number?" in existing_types) # -- ENSURE: Missing type variant is created. schema = "OptionalNumber: {number:Number?}" parser = Parser(schema, existing_types) self.assertTrue("Number?" in existing_types) # -- ENSURE: Newly created type variant is usable. self.assert_match(parser, "OptionalNumber: 42", "number", 42) self.assert_match(parser, "OptionalNumber: 123", "number", 123) self.assert_match(parser, "OptionalNumber: ", "number", None) self.assert_mismatch(parser, "OptionalNumber:") self.assert_mismatch(parser, "OptionalNumber: XXX") self.assert_mismatch(parser, "OptionalNumber: -123") def test_parser__can_parse_cardinality_field_many(self): # -- CARDINALITY: 1..* = one_or_more = many existing_types = dict(Number=parse_number) self.assertFalse("Number+" in existing_types) # -- ENSURE: Missing type variant is created. schema = "List: {numbers:Number+}" parser = Parser(schema, existing_types) self.assertTrue("Number+" in existing_types) # -- ENSURE: Newly created type variant is usable. self.assert_match(parser, "List: 42", "numbers", [42]) self.assert_match(parser, "List: 1, 2, 3", "numbers", [1, 2, 3]) self.assert_match(parser, "List: 4,5,6", "numbers", [4, 5, 6]) self.assert_mismatch(parser, "List: ") self.assert_mismatch(parser, "List:") self.assert_mismatch(parser, "List: XXX") self.assert_mismatch(parser, "List: -123") def test_parser__can_parse_cardinality_field_many_with_own_type_builder(self): # -- CARDINALITY: 1..* = one_or_more = many class MyCardinalityFieldTypeBuilder(CardinalityFieldTypeBuilder): listsep = ';' type_builder = MyCardinalityFieldTypeBuilder existing_types = dict(Number=parse_number) self.assertFalse("Number+" in existing_types) # -- ENSURE: Missing type variant is created. schema = "List: {numbers:Number+}" parser = Parser(schema, existing_types, type_builder=type_builder) self.assertTrue("Number+" in existing_types) # -- ENSURE: Newly created type variant is usable. # NOTE: Use other list separator. self.assert_match(parser, "List: 42", "numbers", [42]) self.assert_match(parser, "List: 1; 2; 3", "numbers", [1, 2, 3]) self.assert_match(parser, "List: 4;5;6", "numbers", [4, 5, 6]) self.assert_mismatch(parser, "List: ") self.assert_mismatch(parser, "List:") self.assert_mismatch(parser, "List: XXX") self.assert_mismatch(parser, "List: -123") def test_parser__can_parse_cardinality_field_many0(self): # -- CARDINALITY: 0..* = zero_or_more = many0 existing_types = dict(Number=parse_number) self.assertFalse("Number*" in existing_types) # -- ENSURE: Missing type variant is created. schema = "List0: {numbers:Number*}" parser = Parser(schema, existing_types) self.assertTrue("Number*" in existing_types) # -- ENSURE: Newly created type variant is usable. self.assert_match(parser, "List0: 42", "numbers", [42]) self.assert_match(parser, "List0: 1, 2, 3", "numbers", [1, 2, 3]) self.assert_match(parser, "List0: ", "numbers", []) self.assert_mismatch(parser, "List0:") self.assert_mismatch(parser, "List0: XXX") self.assert_mismatch(parser, "List0: -123") def test_create_missing_types__without_cardinality_fields_in_schema(self): schemas = ["{}", "{:Number}", "{number3}", "{number4:Number}", "XXX"] existing_types = {} for schema in schemas: new_types = Parser.create_missing_types(schema, existing_types) self.assertEqual(len(new_types), 0) self.assertEqual(new_types, {}) def test_create_missing_types__raises_error_if_primary_type_is_missing(self): # -- HINT: primary type is not provided in type_dict (existing_types) existing_types = {} for schema, missing_types in self.SPECIAL_FIELD_TYPES_DATA: with self.assertRaises(MissingTypeError): Parser.create_missing_types(schema, existing_types) def test_create_missing_types__if_special_types_are_missing(self): existing_types = dict(Number=parse_number) for schema, missing_types in self.SPECIAL_FIELD_TYPES_DATA: new_types = Parser.create_missing_types(schema, existing_types) self.assertSequenceEqual(set(new_types.keys()), set(missing_types)) def test_create_missing_types__if_special_types_exist(self): existing_types = dict(Number=parse_number) for schema, missing_types in self.SPECIAL_FIELD_TYPES_DATA: # -- FIRST STEP: Prepare new_types = Parser.create_missing_types(schema, existing_types) self.assertGreater(len(new_types), 0) # -- SECOND STEP: Now all needed special types should exist. existing_types2 = existing_types.copy() existing_types2.update(new_types) new_types2 = Parser.create_missing_types(schema, existing_types2) self.assertEqual(len(new_types2), 0) # ----------------------------------------------------------------------------- # MAIN: # ----------------------------------------------------------------------------- if __name__ == '__main__': unittest.main() # Copyright (c) 2012-2013 by Jens Engel (https://github/jenisys/parse_type) # # 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. parse_type-0.6.2/tests/test_parse.py000066400000000000000000001263331445072161600175750ustar00rootroot00000000000000# -*- encoding: utf8 -*- # -- BASED-ON: https://github.com/r1chardj0n3s/parse/test_parse.py # VERSION: parse v1.19.1 # Same as original file but uses bundled :mod:`parse_type.parse` module # instead of :mod:`parse` module # # NOTE: Part of the tests are/were providd by jenisys. # -- ORIGINAL-CODE STARTS-HERE ------------------------------------------------ '''Test suite for parse.py This code is copyright 2011 eKit.com Inc (http://www.ekit.com/) See the end of the source file for the license of use. ''' from __future__ import absolute_import import unittest try: import unittest2 as unittest except ImportError: import unittest # -- ADAPTATION-END from datetime import datetime, time from decimal import Decimal import pickle import re # -- EXTENSION: import os PARSE_MODULE = os.environ.get("PARSE_TYPE_PARSE_MODULE", "parse_type") if PARSE_MODULE.startswith("parse_type"): # -- USE EMBEDDED PARSE MODULE: parse_type.parse # HINT: Has either same version or is older than original module. from parse_type import parse else: # -- USE ORIGINAL MODULE: parse import parse # -- EXTENSION-END class TestPattern(unittest.TestCase): def _test_expression(self, format, expression): self.assertEqual(parse.Parser(format)._expression, expression) def test_braces(self): # pull a simple string out of another string self._test_expression('{{ }}', r'\{ \}') def test_fixed(self): # pull a simple string out of another string self._test_expression('{}', r'(.+?)') self._test_expression('{} {}', r'(.+?) (.+?)') def test_named(self): # pull a named string out of another string self._test_expression('{name}', r'(?P.+?)') self._test_expression('{name} {other}', r'(?P.+?) (?P.+?)') def test_named_typed(self): # pull a named string out of another string self._test_expression('{name:w}', r'(?P\w+)') self._test_expression('{name:w} {other:w}', r'(?P\w+) (?P\w+)') def test_numbered(self): self._test_expression('{0}', r'(.+?)') self._test_expression('{0} {1}', r'(.+?) (.+?)') self._test_expression('{0:f} {1:f}', r'([-+ ]?\d*\.\d+) ([-+ ]?\d*\.\d+)') def test_bird(self): # skip some trailing whitespace self._test_expression('{:>}', r' *(.+?)') def test_format_variety(self): def _(fmt, matches): d = parse.extract_format(fmt, {'spam': 'spam'}) for k in matches: self.assertEqual( d.get(k), matches[k], 'm["%s"]=%r, expect %r' % (k, d.get(k), matches[k]), ) for t in '%obxegfdDwWsS': _(t, dict(type=t)) _('10' + t, dict(type=t, width='10')) _('05d', dict(type='d', width='5', zero=True)) _('<', dict(align='<')) _('.<', dict(align='<', fill='.')) _('>', dict(align='>')) _('.>', dict(align='>', fill='.')) _('^', dict(align='^')) _('.^', dict(align='^', fill='.')) _('x=d', dict(type='d', align='=', fill='x')) _('d', dict(type='d')) _('ti', dict(type='ti')) _('spam', dict(type='spam')) _('.^010d', dict(type='d', width='10', align='^', fill='.', zero=True)) _('.2f', dict(type='f', precision='2')) _('10.2f', dict(type='f', width='10', precision='2')) def test_dot_separated_fields(self): # this should just work and provide the named value res = parse.parse('{hello.world}_{jojo.foo.baz}_{simple}', 'a_b_c') assert res.named['hello.world'] == 'a' assert res.named['jojo.foo.baz'] == 'b' assert res.named['simple'] == 'c' def test_dict_style_fields(self): res = parse.parse('{hello[world]}_{hello[foo][baz]}_{simple}', 'a_b_c') assert res.named['hello']['world'] == 'a' assert res.named['hello']['foo']['baz'] == 'b' assert res.named['simple'] == 'c' def test_dot_separated_fields_name_collisions(self): # this should just work and provide the named value res = parse.parse('{a_.b}_{a__b}_{a._b}_{a___b}', 'a_b_c_d') assert res.named['a_.b'] == 'a' assert res.named['a__b'] == 'b' assert res.named['a._b'] == 'c' assert res.named['a___b'] == 'd' def test_invalid_groupnames_are_handled_gracefully(self): self.assertRaises( NotImplementedError, parse.parse, "{hello['world']}", "doesn't work" ) class TestResult(unittest.TestCase): def test_fixed_access(self): r = parse.Result((1, 2), {}, None) self.assertEqual(r[0], 1) self.assertEqual(r[1], 2) self.assertRaises(IndexError, r.__getitem__, 2) self.assertRaises(KeyError, r.__getitem__, 'spam') def test_slice_access(self): r = parse.Result((1, 2, 3, 4), {}, None) self.assertEqual(r[1:3], (2, 3)) self.assertEqual(r[-5:5], (1, 2, 3, 4)) self.assertEqual(r[:4:2], (1, 3)) self.assertEqual(r[::-2], (4, 2)) self.assertEqual(r[5:10], tuple()) def test_named_access(self): r = parse.Result((), {'spam': 'ham'}, None) self.assertEqual(r['spam'], 'ham') self.assertRaises(KeyError, r.__getitem__, 'ham') self.assertRaises(IndexError, r.__getitem__, 0) def test_contains(self): r = parse.Result(('cat',), {'spam': 'ham'}, None) self.assertTrue('spam' in r) self.assertTrue('cat' not in r) self.assertTrue('ham' not in r) class TestParse(unittest.TestCase): def test_no_match(self): # string does not match format self.assertEqual(parse.parse('{{hello}}', 'hello'), None) def test_nothing(self): # do no actual parsing r = parse.parse('{{hello}}', '{hello}') self.assertEqual(r.fixed, ()) self.assertEqual(r.named, {}) def test_no_evaluate_result(self): # pull a fixed value out of string match = parse.parse('hello {}', 'hello world', evaluate_result=False) r = match.evaluate_result() self.assertEqual(r.fixed, ('world',)) def test_regular_expression(self): # match an actual regular expression s = r'^(hello\s[wW]{}!+.*)$' e = s.replace('{}', 'orld') r = parse.parse(s, e) self.assertEqual(r.fixed, ('orld',)) e = s.replace('{}', '.*?') r = parse.parse(s, e) self.assertEqual(r.fixed, ('.*?',)) def test_question_mark(self): # issue9: make sure a ? in the parse string is handled correctly r = parse.parse('"{}"?', '"teststr"?') self.assertEqual(r[0], 'teststr') def test_pipe(self): # issue22: make sure a | in the parse string is handled correctly r = parse.parse('| {}', '| teststr') self.assertEqual(r[0], 'teststr') def test_unicode(self): # issue29: make sure unicode is parsable r = parse.parse('{}', u't€ststr') self.assertEqual(r[0], u't€ststr') def test_hexadecimal(self): # issue42: make sure bare hexadecimal isn't matched as "digits" r = parse.parse('{:d}', 'abcdef') self.assertIsNone(r) def test_fixed(self): # pull a fixed value out of string r = parse.parse('hello {}', 'hello world') self.assertEqual(r.fixed, ('world',)) def test_left(self): # pull left-aligned text out of string r = parse.parse('{:<} world', 'hello world') self.assertEqual(r.fixed, ('hello',)) def test_right(self): # pull right-aligned text out of string r = parse.parse('hello {:>}', 'hello world') self.assertEqual(r.fixed, ('world',)) def test_center(self): # pull center-aligned text out of string r = parse.parse('hello {:^} world', 'hello there world') self.assertEqual(r.fixed, ('there',)) def test_typed(self): # pull a named, typed values out of string r = parse.parse('hello {:d} {:w}', 'hello 12 people') self.assertEqual(r.fixed, (12, 'people')) r = parse.parse('hello {:w} {:w}', 'hello 12 people') self.assertEqual(r.fixed, ('12', 'people')) def test_sign(self): # sign is ignored r = parse.parse('Pi = {:.7f}', 'Pi = 3.1415926') self.assertEqual(r.fixed, (3.1415926,)) r = parse.parse('Pi = {:+.7f}', 'Pi = 3.1415926') self.assertEqual(r.fixed, (3.1415926,)) r = parse.parse('Pi = {:-.7f}', 'Pi = 3.1415926') self.assertEqual(r.fixed, (3.1415926,)) r = parse.parse('Pi = {: .7f}', 'Pi = 3.1415926') self.assertEqual(r.fixed, (3.1415926,)) def test_precision(self): # pull a float out of a string r = parse.parse('Pi = {:.7f}', 'Pi = 3.1415926') self.assertEqual(r.fixed, (3.1415926,)) r = parse.parse('Pi/10 = {:8.5f}', 'Pi/10 = 0.31415') self.assertEqual(r.fixed, (0.31415,)) # float may have not leading zero r = parse.parse('Pi/10 = {:8.5f}', 'Pi/10 = .31415') self.assertEqual(r.fixed, (0.31415,)) r = parse.parse('Pi/10 = {:8.5f}', 'Pi/10 = -.31415') self.assertEqual(r.fixed, (-0.31415,)) def test_custom_type(self): # use a custom type r = parse.parse( '{:shouty} {:spam}', 'hello world', dict(shouty=lambda s: s.upper(), spam=lambda s: ''.join(reversed(s))), ) self.assertEqual(r.fixed, ('HELLO', 'dlrow')) r = parse.parse('{:d}', '12', dict(d=lambda s: int(s) * 2)) self.assertEqual(r.fixed, (24,)) r = parse.parse('{:d}', '12') self.assertEqual(r.fixed, (12,)) def test_typed_fail(self): # pull a named, typed values out of string self.assertEqual(parse.parse('hello {:d} {:w}', 'hello people 12'), None) def test_named(self): # pull a named value out of string r = parse.parse('hello {name}', 'hello world') self.assertEqual(r.named, {'name': 'world'}) def test_named_repeated(self): # test a name may be repeated r = parse.parse('{n} {n}', 'x x') self.assertEqual(r.named, {'n': 'x'}) def test_named_repeated_type(self): # test a name may be repeated with type conversion r = parse.parse('{n:d} {n:d}', '1 1') self.assertEqual(r.named, {'n': 1}) def test_named_repeated_fail_value(self): # test repeated name fails if value mismatches r = parse.parse('{n} {n}', 'x y') self.assertEqual(r, None) def test_named_repeated_type_fail_value(self): # test repeated name with type conversion fails if value mismatches r = parse.parse('{n:d} {n:d}', '1 2') self.assertEqual(r, None) def test_named_repeated_type_mismatch(self): # test repeated name with mismatched type self.assertRaises(parse.RepeatedNameError, parse.compile, '{n:d} {n:w}') def test_mixed(self): # pull a fixed and named values out of string r = parse.parse('hello {} {name} {} {spam}', 'hello world and other beings') self.assertEqual(r.fixed, ('world', 'other')) self.assertEqual(r.named, dict(name='and', spam='beings')) def test_named_typed(self): # pull a named, typed values out of string r = parse.parse('hello {number:d} {things}', 'hello 12 people') self.assertEqual(r.named, dict(number=12, things='people')) r = parse.parse('hello {number:w} {things}', 'hello 12 people') self.assertEqual(r.named, dict(number='12', things='people')) def test_named_aligned_typed(self): # pull a named, typed values out of string r = parse.parse('hello {number:d} {things}', 'hello 12 people') self.assertEqual(r.named, dict(number=12, things='people')) r = parse.parse('hello {number:^d} {things}', 'hello 12 people') self.assertEqual(r.named, dict(number=12, things='people')) def test_multiline(self): r = parse.parse('hello\n{}\nworld', 'hello\nthere\nworld') self.assertEqual(r.fixed[0], 'there') def test_spans(self): # test the string sections our fields come from string = 'hello world' r = parse.parse('hello {}', string) self.assertEqual(r.spans, {0: (6, 11)}) start, end = r.spans[0] self.assertEqual(string[start:end], r.fixed[0]) string = 'hello world' r = parse.parse('hello {:>}', string) self.assertEqual(r.spans, {0: (10, 15)}) start, end = r.spans[0] self.assertEqual(string[start:end], r.fixed[0]) string = 'hello 0x12 world' r = parse.parse('hello {val:x} world', string) self.assertEqual(r.spans, {'val': (6, 10)}) start, end = r.spans['val'] self.assertEqual(string[start:end], '0x%x' % r.named['val']) string = 'hello world and other beings' r = parse.parse('hello {} {name} {} {spam}', string) self.assertEqual( r.spans, {0: (6, 11), 'name': (12, 15), 1: (16, 21), 'spam': (22, 28)} ) def test_numbers(self): # pull a numbers out of a string def y(fmt, s, e, str_equals=False): p = parse.compile(fmt) r = p.parse(s) if r is None: self.fail('%r (%r) did not match %r' % (fmt, p._expression, s)) r = r.fixed[0] if str_equals: self.assertEqual( str(r), str(e), '%r found %r in %r, not %r' % (fmt, r, s, e) ) else: self.assertEqual(r, e, '%r found %r in %r, not %r' % (fmt, r, s, e)) def n(fmt, s, e): if parse.parse(fmt, s) is not None: self.fail('%r matched %r' % (fmt, s)) y('a {:d} b', 'a 0 b', 0) y('a {:d} b', 'a 12 b', 12) y('a {:5d} b', 'a 12 b', 12) y('a {:5d} b', 'a -12 b', -12) y('a {:d} b', 'a -12 b', -12) y('a {:d} b', 'a +12 b', 12) y('a {:d} b', 'a 12 b', 12) y('a {:d} b', 'a 0b1000 b', 8) y('a {:d} b', 'a 0o1000 b', 512) y('a {:d} b', 'a 0x1000 b', 4096) y('a {:d} b', 'a 0xabcdef b', 0xABCDEF) y('a {:%} b', 'a 100% b', 1) y('a {:%} b', 'a 50% b', 0.5) y('a {:%} b', 'a 50.1% b', 0.501) y('a {:n} b', 'a 100 b', 100) y('a {:n} b', 'a 1,000 b', 1000) y('a {:n} b', 'a 1.000 b', 1000) y('a {:n} b', 'a -1,000 b', -1000) y('a {:n} b', 'a 10,000 b', 10000) y('a {:n} b', 'a 100,000 b', 100000) n('a {:n} b', 'a 100,00 b', None) y('a {:n} b', 'a 100.000 b', 100000) y('a {:n} b', 'a 1.000.000 b', 1000000) y('a {:f} b', 'a 12.0 b', 12.0) y('a {:f} b', 'a -12.1 b', -12.1) y('a {:f} b', 'a +12.1 b', 12.1) y('a {:f} b', 'a .121 b', 0.121) y('a {:f} b', 'a -.121 b', -0.121) n('a {:f} b', 'a 12 b', None) y('a {:e} b', 'a 1.0e10 b', 1.0e10) y('a {:e} b', 'a .0e10 b', 0.0e10) y('a {:e} b', 'a 1.0E10 b', 1.0e10) y('a {:e} b', 'a 1.10000e10 b', 1.1e10) y('a {:e} b', 'a 1.0e-10 b', 1.0e-10) y('a {:e} b', 'a 1.0e+10 b', 1.0e10) # can't actually test this one on values 'cos nan != nan y('a {:e} b', 'a nan b', float('nan'), str_equals=True) y('a {:e} b', 'a NAN b', float('nan'), str_equals=True) y('a {:e} b', 'a inf b', float('inf')) y('a {:e} b', 'a +inf b', float('inf')) y('a {:e} b', 'a -inf b', float('-inf')) y('a {:e} b', 'a INF b', float('inf')) y('a {:e} b', 'a +INF b', float('inf')) y('a {:e} b', 'a -INF b', float('-inf')) y('a {:g} b', 'a 1 b', 1) y('a {:g} b', 'a 1e10 b', 1e10) y('a {:g} b', 'a 1.0e10 b', 1.0e10) y('a {:g} b', 'a 1.0E10 b', 1.0e10) y('a {:b} b', 'a 1000 b', 8) y('a {:b} b', 'a 0b1000 b', 8) y('a {:o} b', 'a 12345670 b', int('12345670', 8)) y('a {:o} b', 'a 0o12345670 b', int('12345670', 8)) y('a {:x} b', 'a 1234567890abcdef b', 0x1234567890ABCDEF) y('a {:x} b', 'a 1234567890ABCDEF b', 0x1234567890ABCDEF) y('a {:x} b', 'a 0x1234567890abcdef b', 0x1234567890ABCDEF) y('a {:x} b', 'a 0x1234567890ABCDEF b', 0x1234567890ABCDEF) y('a {:05d} b', 'a 00001 b', 1) y('a {:05d} b', 'a -00001 b', -1) y('a {:05d} b', 'a +00001 b', 1) y('a {:02d} b', 'a 10 b', 10) y('a {:=d} b', 'a 000012 b', 12) y('a {:x=5d} b', 'a xxx12 b', 12) y('a {:x=5d} b', 'a -xxx12 b', -12) # Test that hex numbers that ambiguously start with 0b / 0B are parsed correctly # See issue #65 (https://github.com/r1chardj0n3s/parse/issues/65) y('a {:x} b', 'a 0B b', 0xB) y('a {:x} b', 'a 0B1 b', 0xB1) y('a {:x} b', 'a 0b b', 0xB) y('a {:x} b', 'a 0b1 b', 0xB1) # Test that number signs are understood correctly y('a {:d} b', 'a -0o10 b', -8) y('a {:d} b', 'a -0b1010 b', -10) y('a {:d} b', 'a -0x1010 b', -0x1010) y('a {:o} b', 'a -10 b', -8) y('a {:b} b', 'a -1010 b', -10) y('a {:x} b', 'a -1010 b', -0x1010) y('a {:d} b', 'a +0o10 b', 8) y('a {:d} b', 'a +0b1010 b', 10) y('a {:d} b', 'a +0x1010 b', 0x1010) y('a {:o} b', 'a +10 b', 8) y('a {:b} b', 'a +1010 b', 10) y('a {:x} b', 'a +1010 b', 0x1010) def test_two_datetimes(self): r = parse.parse('a {:ti} {:ti} b', 'a 1997-07-16 2012-08-01 b') self.assertEqual(len(r.fixed), 2) self.assertEqual(r[0], datetime(1997, 7, 16)) self.assertEqual(r[1], datetime(2012, 8, 1)) def test_datetimes(self): def y(fmt, s, e, tz=None): p = parse.compile(fmt) r = p.parse(s) if r is None: self.fail('%r (%r) did not match %r' % (fmt, p._expression, s)) r = r.fixed[0] try: self.assertEqual(r, e, '%r found %r in %r, not %r' % (fmt, r, s, e)) except ValueError: self.fail('%r found %r in %r, not %r' % (fmt, r, s, e)) if tz is not None: self.assertEqual( r.tzinfo, tz, '%r found TZ %r in %r, not %r' % (fmt, r.tzinfo, s, e) ) def n(fmt, s, e): if parse.parse(fmt, s) is not None: self.fail('%r matched %r' % (fmt, s)) utc = parse.FixedTzOffset(0, 'UTC') aest = parse.FixedTzOffset(10 * 60, '+1000') tz60 = parse.FixedTzOffset(60, '+01:00') # ISO 8660 variants # YYYY-MM-DD (eg 1997-07-16) y('a {:ti} b', 'a 1997-07-16 b', datetime(1997, 7, 16)) # YYYY-MM-DDThh:mmTZD (eg 1997-07-16T19:20+01:00) y('a {:ti} b', 'a 1997-07-16 19:20 b', datetime(1997, 7, 16, 19, 20, 0)) y('a {:ti} b', 'a 1997-07-16T19:20 b', datetime(1997, 7, 16, 19, 20, 0)) y( 'a {:ti} b', 'a 1997-07-16T19:20Z b', datetime(1997, 7, 16, 19, 20, tzinfo=utc), ) y( 'a {:ti} b', 'a 1997-07-16T19:20+0100 b', datetime(1997, 7, 16, 19, 20, tzinfo=tz60), ) y( 'a {:ti} b', 'a 1997-07-16T19:20+01:00 b', datetime(1997, 7, 16, 19, 20, tzinfo=tz60), ) y( 'a {:ti} b', 'a 1997-07-16T19:20 +01:00 b', datetime(1997, 7, 16, 19, 20, tzinfo=tz60), ) # YYYY-MM-DDThh:mm:ssTZD (eg 1997-07-16T19:20:30+01:00) y('a {:ti} b', 'a 1997-07-16 19:20:30 b', datetime(1997, 7, 16, 19, 20, 30)) y('a {:ti} b', 'a 1997-07-16T19:20:30 b', datetime(1997, 7, 16, 19, 20, 30)) y( 'a {:ti} b', 'a 1997-07-16T19:20:30Z b', datetime(1997, 7, 16, 19, 20, 30, tzinfo=utc), ) y( 'a {:ti} b', 'a 1997-07-16T19:20:30+01:00 b', datetime(1997, 7, 16, 19, 20, 30, tzinfo=tz60), ) y( 'a {:ti} b', 'a 1997-07-16T19:20:30 +01:00 b', datetime(1997, 7, 16, 19, 20, 30, tzinfo=tz60), ) # YYYY-MM-DDThh:mm:ss.sTZD (eg 1997-07-16T19:20:30.45+01:00) y( 'a {:ti} b', 'a 1997-07-16 19:20:30.500000 b', datetime(1997, 7, 16, 19, 20, 30, 500000), ) y( 'a {:ti} b', 'a 1997-07-16T19:20:30.500000 b', datetime(1997, 7, 16, 19, 20, 30, 500000), ) y( 'a {:ti} b', 'a 1997-07-16T19:20:30.5Z b', datetime(1997, 7, 16, 19, 20, 30, 500000, tzinfo=utc), ) y( 'a {:ti} b', 'a 1997-07-16T19:20:30.5+01:00 b', datetime(1997, 7, 16, 19, 20, 30, 500000, tzinfo=tz60), ) aest_d = datetime(2011, 11, 21, 10, 21, 36, tzinfo=aest) dt = datetime(2011, 11, 21, 10, 21, 36) dt00 = datetime(2011, 11, 21, 10, 21) d = datetime(2011, 11, 21) # te RFC2822 e-mail format datetime y('a {:te} b', 'a Mon, 21 Nov 2011 10:21:36 +1000 b', aest_d) y('a {:te} b', 'a Mon, 21 Nov 2011 10:21:36 +10:00 b', aest_d) y('a {:te} b', 'a 21 Nov 2011 10:21:36 +1000 b', aest_d) # tg global (day/month) format datetime y('a {:tg} b', 'a 21/11/2011 10:21:36 AM +1000 b', aest_d) y('a {:tg} b', 'a 21/11/2011 10:21:36 AM +10:00 b', aest_d) y('a {:tg} b', 'a 21-11-2011 10:21:36 AM +1000 b', aest_d) y('a {:tg} b', 'a 21/11/2011 10:21:36 +1000 b', aest_d) y('a {:tg} b', 'a 21/11/2011 10:21:36 b', dt) y('a {:tg} b', 'a 21/11/2011 10:21 b', dt00) y('a {:tg} b', 'a 21-11-2011 b', d) y('a {:tg} b', 'a 21-Nov-2011 10:21:36 AM +1000 b', aest_d) y('a {:tg} b', 'a 21-November-2011 10:21:36 AM +1000 b', aest_d) # ta US (month/day) format datetime y('a {:ta} b', 'a 11/21/2011 10:21:36 AM +1000 b', aest_d) y('a {:ta} b', 'a 11/21/2011 10:21:36 AM +10:00 b', aest_d) y('a {:ta} b', 'a 11-21-2011 10:21:36 AM +1000 b', aest_d) y('a {:ta} b', 'a 11/21/2011 10:21:36 +1000 b', aest_d) y('a {:ta} b', 'a 11/21/2011 10:21:36 b', dt) y('a {:ta} b', 'a 11/21/2011 10:21 b', dt00) y('a {:ta} b', 'a 11-21-2011 b', d) y('a {:ta} b', 'a Nov-21-2011 10:21:36 AM +1000 b', aest_d) y('a {:ta} b', 'a November-21-2011 10:21:36 AM +1000 b', aest_d) y('a {:ta} b', 'a November-21-2011 b', d) # ts Linux System log format datetime y( 'a {:ts} b', 'a Nov 21 10:21:36 b', datetime(datetime.today().year, 11, 21, 10, 21, 36), ) y( 'a {:ts} b', 'a Nov 1 10:21:36 b', datetime(datetime.today().year, 11, 1, 10, 21, 36), ) y( 'a {:ts} b', 'a Nov 1 03:21:36 b', datetime(datetime.today().year, 11, 1, 3, 21, 36), ) # th HTTP log format date/time datetime y('a {:th} b', 'a 21/Nov/2011:10:21:36 +1000 b', aest_d) y('a {:th} b', 'a 21/Nov/2011:10:21:36 +10:00 b', aest_d) d = datetime(2011, 11, 21, 10, 21, 36) # tc ctime() format datetime y('a {:tc} b', 'a Mon Nov 21 10:21:36 2011 b', d) t530 = parse.FixedTzOffset(-5 * 60 - 30, '-5:30') t830 = parse.FixedTzOffset(-8 * 60 - 30, '-8:30') # tt Time time y('a {:tt} b', 'a 10:21:36 AM +1000 b', time(10, 21, 36, tzinfo=aest)) y('a {:tt} b', 'a 10:21:36 AM +10:00 b', time(10, 21, 36, tzinfo=aest)) y('a {:tt} b', 'a 10:21:36 AM b', time(10, 21, 36)) y('a {:tt} b', 'a 10:21:36 PM b', time(22, 21, 36)) y('a {:tt} b', 'a 10:21:36 b', time(10, 21, 36)) y('a {:tt} b', 'a 10:21 b', time(10, 21)) y('a {:tt} b', 'a 10:21:36 PM -5:30 b', time(22, 21, 36, tzinfo=t530)) y('a {:tt} b', 'a 10:21:36 PM -530 b', time(22, 21, 36, tzinfo=t530)) y('a {:tt} b', 'a 10:21:36 PM -05:30 b', time(22, 21, 36, tzinfo=t530)) y('a {:tt} b', 'a 10:21:36 PM -0530 b', time(22, 21, 36, tzinfo=t530)) y('a {:tt} b', 'a 10:21:36 PM -08:30 b', time(22, 21, 36, tzinfo=t830)) y('a {:tt} b', 'a 10:21:36 PM -0830 b', time(22, 21, 36, tzinfo=t830)) def test_datetime_group_count(self): # test we increment the group count correctly for datetimes r = parse.parse('{:ti} {}', '1972-01-01 spam') self.assertEqual(r.fixed[1], 'spam') r = parse.parse('{:tg} {}', '1-1-1972 spam') self.assertEqual(r.fixed[1], 'spam') r = parse.parse('{:ta} {}', '1-1-1972 spam') self.assertEqual(r.fixed[1], 'spam') r = parse.parse('{:th} {}', '21/Nov/2011:10:21:36 +1000 spam') self.assertEqual(r.fixed[1], 'spam') r = parse.parse('{:te} {}', '21 Nov 2011 10:21:36 +1000 spam') self.assertEqual(r.fixed[1], 'spam') r = parse.parse('{:tc} {}', 'Mon Nov 21 10:21:36 2011 spam') self.assertEqual(r.fixed[1], 'spam') r = parse.parse('{:tt} {}', '10:21 spam') self.assertEqual(r.fixed[1], 'spam') def test_mixed_types(self): # stress-test: pull one of everything out of a string r = parse.parse( ''' letters: {:w} non-letters: {:W} whitespace: "{:s}" non-whitespace: \t{:S}\n digits: {:d} {:d} non-digits: {:D} numbers with thousands: {:n} fixed-point: {:f} floating-point: {:e} general numbers: {:g} {:g} binary: {:b} octal: {:o} hex: {:x} ISO 8601 e.g. {:ti} RFC2822 e.g. {:te} Global e.g. {:tg} US e.g. {:ta} ctime() e.g. {:tc} HTTP e.g. {:th} time: {:tt} final value: {} ''', ''' letters: abcdef_GHIJLK non-letters: !@#%$ *^% whitespace: " \t\n" non-whitespace: \tabc\n digits: 12345 0b1011011 non-digits: abcdef numbers with thousands: 1,000 fixed-point: 100.2345 floating-point: 1.1e-10 general numbers: 1 1.1 binary: 0b1000 octal: 0o1000 hex: 0x1000 ISO 8601 e.g. 1972-01-20T10:21:36Z RFC2822 e.g. Mon, 20 Jan 1972 10:21:36 +1000 Global e.g. 20/1/1972 10:21:36 AM +1:00 US e.g. 1/20/1972 10:21:36 PM +10:30 ctime() e.g. Sun Sep 16 01:03:52 1973 HTTP e.g. 21/Nov/2011:00:07:11 +0000 time: 10:21:36 PM -5:30 final value: spam ''', ) self.assertNotEqual(r, None) self.assertEqual(r.fixed[22], 'spam') def test_mixed_type_variant(self): r = parse.parse( ''' letters: {:w} non-letters: {:W} whitespace: "{:s}" non-whitespace: \t{:S}\n digits: {:d} non-digits: {:D} numbers with thousands: {:n} fixed-point: {:f} floating-point: {:e} general numbers: {:g} {:g} binary: {:b} octal: {:o} hex: {:x} ISO 8601 e.g. {:ti} RFC2822 e.g. {:te} Global e.g. {:tg} US e.g. {:ta} ctime() e.g. {:tc} HTTP e.g. {:th} time: {:tt} final value: {} ''', ''' letters: abcdef_GHIJLK non-letters: !@#%$ *^% whitespace: " \t\n" non-whitespace: \tabc\n digits: 0xabcdef non-digits: abcdef numbers with thousands: 1.000.000 fixed-point: 0.00001 floating-point: NAN general numbers: 1.1e10 nan binary: 0B1000 octal: 0O1000 hex: 0X1000 ISO 8601 e.g. 1972-01-20T10:21:36Z RFC2822 e.g. Mon, 20 Jan 1972 10:21:36 +1000 Global e.g. 20/1/1972 10:21:36 AM +1:00 US e.g. 1/20/1972 10:21:36 PM +10:30 ctime() e.g. Sun Sep 16 01:03:52 1973 HTTP e.g. 21/Nov/2011:00:07:11 +0000 time: 10:21:36 PM -5:30 final value: spam ''', ) self.assertNotEqual(r, None) self.assertEqual(r.fixed[21], 'spam') def test_too_many_fields(self): # Python 3.5 removed the limit of 100 named groups in a regular expression, # so only test for the exception if the limit exists. try: re.compile("".join("(?P{n}-)".format(n=i) for i in range(101))) except AssertionError: p = parse.compile('{:ti}' * 15) self.assertRaises(parse.TooManyFields, p.parse, '') def test_letters(self): res = parse.parse('{:l}', '') self.assertIsNone(res) res = parse.parse('{:l}', 'sPaM') self.assertEqual(res.fixed, ('sPaM',)) res = parse.parse('{:l}', 'sP4M') self.assertIsNone(res) res = parse.parse('{:l}', 'sP_M') self.assertIsNone(res) class TestSearch(unittest.TestCase): def test_basic(self): # basic search() test r = parse.search('a {} c', ' a b c ') self.assertEqual(r.fixed, ('b',)) def test_multiline(self): # multiline search() test r = parse.search('age: {:d}\n', 'name: Rufus\nage: 42\ncolor: red\n') self.assertEqual(r.fixed, (42,)) def test_pos(self): # basic search() test r = parse.search('a {} c', ' a b c ', 2) self.assertEqual(r, None) def test_no_evaluate_result(self): match = parse.search( 'age: {:d}\n', 'name: Rufus\nage: 42\ncolor: red\n', evaluate_result=False ) r = match.evaluate_result() self.assertEqual(r.fixed, (42,)) class TestFindall(unittest.TestCase): def test_findall(self): # basic findall() test s = ''.join( r.fixed[0] for r in parse.findall(">{}<", "

    some bold text

    ") ) self.assertEqual(s, "some bold text") def test_no_evaluate_result(self): # basic findall() test s = ''.join( m.evaluate_result().fixed[0] for m in parse.findall( ">{}<", "

    some bold text

    ", evaluate_result=False ) ) self.assertEqual(s, "some bold text") def test_case_sensitivity(self): l = [r.fixed[0] for r in parse.findall("x({})x", "X(hi)X")] self.assertEqual(l, ["hi"]) l = [r.fixed[0] for r in parse.findall("x({})x", "X(hi)X", case_sensitive=True)] self.assertEqual(l, []) class TestBugs(unittest.TestCase): def test_tz_compare_to_None(self): utc = parse.FixedTzOffset(0, 'UTC') self.assertNotEqual(utc, None) self.assertNotEqual(utc, 'spam') def test_named_date_issue7(self): r = parse.parse('on {date:ti}', 'on 2012-09-17') self.assertEqual(r['date'], datetime(2012, 9, 17, 0, 0, 0)) # fix introduced regressions r = parse.parse('a {:ti} b', 'a 1997-07-16T19:20 b') self.assertEqual(r[0], datetime(1997, 7, 16, 19, 20, 0)) r = parse.parse('a {:ti} b', 'a 1997-07-16T19:20Z b') utc = parse.FixedTzOffset(0, 'UTC') self.assertEqual(r[0], datetime(1997, 7, 16, 19, 20, tzinfo=utc)) r = parse.parse('a {date:ti} b', 'a 1997-07-16T19:20Z b') self.assertEqual(r['date'], datetime(1997, 7, 16, 19, 20, tzinfo=utc)) def test_dotted_type_conversion_pull_8(self): # test pull request 8 which fixes type conversion related to dotted # names being applied correctly r = parse.parse('{a.b:d}', '1') self.assertEqual(r['a.b'], 1) r = parse.parse('{a_b:w} {a.b:d}', '1 2') self.assertEqual(r['a_b'], '1') self.assertEqual(r['a.b'], 2) def test_pm_overflow_issue16(self): r = parse.parse('Meet at {:tg}', 'Meet at 1/2/2011 12:45 PM') self.assertEqual(r[0], datetime(2011, 2, 1, 12, 45)) def test_pm_handling_issue57(self): r = parse.parse('Meet at {:tg}', 'Meet at 1/2/2011 12:15 PM') self.assertEqual(r[0], datetime(2011, 2, 1, 12, 15)) r = parse.parse('Meet at {:tg}', 'Meet at 1/2/2011 12:15 AM') self.assertEqual(r[0], datetime(2011, 2, 1, 0, 15)) def test_user_type_with_group_count_issue60(self): @parse.with_pattern(r'((\w+))', regex_group_count=2) def parse_word_and_covert_to_uppercase(text): return text.strip().upper() @parse.with_pattern(r'\d+') def parse_number(text): return int(text) # -- CASE: Use named (OK) type_map = dict(Name=parse_word_and_covert_to_uppercase, Number=parse_number) r = parse.parse( 'Hello {name:Name} {number:Number}', 'Hello Alice 42', extra_types=type_map ) self.assertEqual(r.named, dict(name='ALICE', number=42)) # -- CASE: Use unnamed/fixed (problematic) r = parse.parse( 'Hello {:Name} {:Number}', 'Hello Alice 42', extra_types=type_map ) self.assertEqual(r[0], 'ALICE') self.assertEqual(r[1], 42) def test_unmatched_brace_doesnt_match(self): r = parse.parse("{who.txt", "hello") self.assertIsNone(r) def test_pickling_bug_110(self): p = parse.compile('{a:d}') # prior to the fix, this would raise an AttributeError pickle.dumps(p) def test_unused_centered_alignment_bug(self): r = parse.parse("{:^2S}", "foo") self.assertEqual(r[0], "foo") r = parse.search("{:^2S}", "foo") self.assertEqual(r[0], "foo") # specifically test for the case in issue #118 as well r = parse.parse("Column {:d}:{:^}", "Column 1: Timestep") self.assertEqual(r[0], 1) self.assertEqual(r[1], "Timestep") def test_unused_left_alignment_bug(self): r = parse.parse("{:<2S}", "foo") self.assertEqual(r[0], "foo") r = parse.search("{:<2S}", "foo") self.assertEqual(r[0], "foo") def test_match_trailing_newline(self): r = parse.parse('{}', 'test\n') self.assertEqual(r[0], 'test\n') # ----------------------------------------------------------------------------- # TEST SUPPORT FOR: TestParseType # ----------------------------------------------------------------------------- class TestParseType(unittest.TestCase): def assert_match(self, parser, text, param_name, expected): result = parser.parse(text) self.assertEqual(result[param_name], expected) def assert_mismatch(self, parser, text, param_name): result = parser.parse(text) self.assertTrue(result is None) def assert_fixed_match(self, parser, text, expected): result = parser.parse(text) self.assertEqual(result.fixed, expected) def assert_fixed_mismatch(self, parser, text): result = parser.parse(text) self.assertEqual(result, None) def test_pattern_should_be_used(self): def parse_number(text): return int(text) parse_number.pattern = r"\d+" parse_number.name = "Number" # For testing only. extra_types = {parse_number.name: parse_number} format = "Value is {number:Number} and..." parser = parse.Parser(format, extra_types) self.assert_match(parser, "Value is 42 and...", "number", 42) self.assert_match(parser, "Value is 00123 and...", "number", 123) self.assert_mismatch(parser, "Value is ALICE and...", "number") self.assert_mismatch(parser, "Value is -123 and...", "number") def test_pattern_should_be_used2(self): def parse_yesno(text): return parse_yesno.mapping[text.lower()] parse_yesno.mapping = { "yes": True, "no": False, "on": True, "off": False, "true": True, "false": False, } parse_yesno.pattern = r"|".join(parse_yesno.mapping.keys()) parse_yesno.name = "YesNo" # For testing only. extra_types = {parse_yesno.name: parse_yesno} format = "Answer: {answer:YesNo}" parser = parse.Parser(format, extra_types) # -- ENSURE: Known enum values are correctly extracted. for value_name, value in parse_yesno.mapping.items(): text = "Answer: %s" % value_name self.assert_match(parser, text, "answer", value) # -- IGNORE-CASE: In parsing, calls type converter function !!! self.assert_match(parser, "Answer: YES", "answer", True) self.assert_mismatch(parser, "Answer: __YES__", "answer") def test_with_pattern(self): ab_vals = dict(a=1, b=2) @parse.with_pattern(r'[ab]') def ab(text): return ab_vals[text] parser = parse.Parser('test {result:ab}', {'ab': ab}) self.assert_match(parser, 'test a', 'result', 1) self.assert_match(parser, 'test b', 'result', 2) self.assert_mismatch(parser, "test c", "result") def test_with_pattern_and_regex_group_count(self): # -- SPECIAL-CASE: Regex-grouping is used in user-defined type # NOTE: Missing or wroung regex_group_counts cause problems # with parsing following params. @parse.with_pattern(r'(meter|kilometer)', regex_group_count=1) def parse_unit(text): return text.strip() @parse.with_pattern(r'\d+') def parse_number(text): return int(text) type_converters = dict(Number=parse_number, Unit=parse_unit) # -- CASE: Unnamed-params (affected) parser = parse.Parser('test {:Unit}-{:Number}', type_converters) self.assert_fixed_match(parser, 'test meter-10', ('meter', 10)) self.assert_fixed_match(parser, 'test kilometer-20', ('kilometer', 20)) self.assert_fixed_mismatch(parser, 'test liter-30') # -- CASE: Named-params (uncritical; should not be affected) # REASON: Named-params have additional, own grouping. parser2 = parse.Parser('test {unit:Unit}-{value:Number}', type_converters) self.assert_match(parser2, 'test meter-10', 'unit', 'meter') self.assert_match(parser2, 'test meter-10', 'value', 10) self.assert_match(parser2, 'test kilometer-20', 'unit', 'kilometer') self.assert_match(parser2, 'test kilometer-20', 'value', 20) self.assert_mismatch(parser2, 'test liter-30', 'unit') def test_with_pattern_and_wrong_regex_group_count_raises_error(self): # -- SPECIAL-CASE: # Regex-grouping is used in user-defined type, but wrong value is provided. @parse.with_pattern(r'(meter|kilometer)', regex_group_count=1) def parse_unit(text): return text.strip() @parse.with_pattern(r'\d+') def parse_number(text): return int(text) # -- CASE: Unnamed-params (affected) BAD_REGEX_GROUP_COUNTS_AND_ERRORS = [ (None, ValueError), (0, ValueError), (2, IndexError), ] for bad_regex_group_count, error_class in BAD_REGEX_GROUP_COUNTS_AND_ERRORS: parse_unit.regex_group_count = bad_regex_group_count # -- OVERRIDE-HERE type_converters = dict(Number=parse_number, Unit=parse_unit) parser = parse.Parser('test {:Unit}-{:Number}', type_converters) self.assertRaises(error_class, parser.parse, 'test meter-10') def test_with_pattern_and_regex_group_count_is_none(self): # -- CORNER-CASE: Increase code-coverage. data_values = dict(a=1, b=2) @parse.with_pattern(r'[ab]') def parse_data(text): return data_values[text] parse_data.regex_group_count = None # ENFORCE: None # -- CASE: Unnamed-params parser = parse.Parser('test {:Data}', {'Data': parse_data}) self.assert_fixed_match(parser, 'test a', (1,)) self.assert_fixed_match(parser, 'test b', (2,)) self.assert_fixed_mismatch(parser, 'test c') # -- CASE: Named-params parser2 = parse.Parser('test {value:Data}', {'Data': parse_data}) self.assert_match(parser2, 'test a', 'value', 1) self.assert_match(parser2, 'test b', 'value', 2) self.assert_mismatch(parser2, 'test c', 'value') def test_case_sensitivity(self): r = parse.parse('SPAM {} SPAM', 'spam spam spam') self.assertEqual(r[0], 'spam') self.assertEqual( parse.parse('SPAM {} SPAM', 'spam spam spam', case_sensitive=True), None ) def test_decimal_value(self): value = Decimal('5.5') str_ = 'test {}'.format(value) parser = parse.Parser('test {:F}') self.assertEqual(parser.parse(str_)[0], value) def test_width_str(self): res = parse.parse('{:.2}{:.2}', 'look') self.assertEqual(res.fixed, ('lo', 'ok')) res = parse.parse('{:2}{:2}', 'look') self.assertEqual(res.fixed, ('lo', 'ok')) res = parse.parse('{:4}{}', 'look at that') self.assertEqual(res.fixed, ('look', ' at that')) def test_width_constraints(self): res = parse.parse('{:4}', 'looky') self.assertEqual(res.fixed, ('looky',)) res = parse.parse('{:4.4}', 'looky') self.assertIsNone(res) res = parse.parse('{:4.4}', 'ook') self.assertIsNone(res) res = parse.parse('{:4}{:.4}', 'look at that') self.assertEqual(res.fixed, ('look at ', 'that')) def test_width_multi_int(self): res = parse.parse('{:02d}{:02d}', '0440') self.assertEqual(res.fixed, (4, 40)) res = parse.parse('{:03d}{:d}', '04404') self.assertEqual(res.fixed, (44, 4)) def test_width_empty_input(self): res = parse.parse('{:.2}', '') self.assertIsNone(res) res = parse.parse('{:2}', 'l') self.assertIsNone(res) res = parse.parse('{:2d}', '') self.assertIsNone(res) def test_int_convert_stateless_base(self): parser = parse.Parser("{:d}") self.assertEqual(parser.parse("1234")[0], 1234) self.assertEqual(parser.parse("0b1011")[0], 0b1011) if __name__ == '__main__': unittest.main() # Copyright (c) 2011 eKit.com Inc (http://www.ekit.com/) # # 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. # vim: set filetype=python ts=4 sw=4 et si tw=75 parse_type-0.6.2/tests/test_parse_decorator.py000077500000000000000000000130701445072161600216330ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # pylint: disable=invalid-name, missing-docstring, too-few-public-methods """ Integrated into :mod:`parse` module. """ from __future__ import absolute_import import unittest import parse from parse_type import build_type_dict from .parse_type_test import ParseTypeTestCase # ----------------------------------------------------------------------------- # TEST CASE: TestParseTypeWithPatternDecorator # ----------------------------------------------------------------------------- class TestParseTypeWithPatternDecorator(ParseTypeTestCase): r""" Test the pattern decorator for type-converter (parse_type) functions. >>> def parse_number(text): ... return int(text) >>> parse_number.pattern = r"\d+" is equivalent to: >>> import parse >>> @parse.with_pattern(r"\d+") ... def parse_number(text): ... return int(text) >>> assert hasattr(parse_number, "pattern") >>> assert parse_number.pattern == r"\d+" """ def assert_decorated_with_pattern(self, func, expected_pattern): self.assertTrue(callable(func)) self.assertTrue(hasattr(func, "pattern")) self.assertEqual(func.pattern, expected_pattern) def assert_converter_call(self, func, text, expected_value): value = func(text) self.assertEqual(value, expected_value) # -- TESTS: def test_function_with_pattern_decorator(self): @parse.with_pattern(r"\d+") def parse_number(text): return int(text) self.assert_decorated_with_pattern(parse_number, r"\d+") self.assert_converter_call(parse_number, "123", 123) def test_classmethod_with_pattern_decorator(self): choice_pattern = r"Alice|Bob|Charly" class C(object): @classmethod @parse.with_pattern(choice_pattern) def parse_choice(cls, text): return text self.assert_decorated_with_pattern(C.parse_choice, choice_pattern) self.assert_converter_call(C.parse_choice, "Alice", "Alice") def test_staticmethod_with_pattern_decorator(self): choice_pattern = r"Alice|Bob|Charly" class S(object): @staticmethod @parse.with_pattern(choice_pattern) def parse_choice(text): return text self.assert_decorated_with_pattern(S.parse_choice, choice_pattern) self.assert_converter_call(S.parse_choice, "Bob", "Bob") def test_decorated_function_with_parser(self): # -- SETUP: @parse.with_pattern(r"\d+") def parse_number(text): return int(text) parse_number.name = "Number" #< For test automation. more_types = build_type_dict([parse_number]) schema = "Test: {number:Number}" parser = parse.Parser(schema, more_types) # -- PERFORM TESTS: # pylint: disable=bad-whitespace self.assert_match(parser, "Test: 1", "number", 1) self.assert_match(parser, "Test: 42", "number", 42) self.assert_match(parser, "Test: 123", "number", 123) # -- PARSE MISMATCH: self.assert_mismatch(parser, "Test: x", "number") # Not a Number. self.assert_mismatch(parser, "Test: -1", "number") # Negative. self.assert_mismatch(parser, "Test: a, b", "number") # List of ... def test_decorated_classmethod_with_parser(self): # -- SETUP: class C(object): @classmethod @parse.with_pattern(r"Alice|Bob|Charly") def parse_person(cls, text): return text more_types = {"Person": C.parse_person} schema = "Test: {person:Person}" parser = parse.Parser(schema, more_types) # -- PERFORM TESTS: # pylint: disable=bad-whitespace self.assert_match(parser, "Test: Alice", "person", "Alice") self.assert_match(parser, "Test: Bob", "person", "Bob") # -- PARSE MISMATCH: self.assert_mismatch(parser, "Test: ", "person") # Missing. self.assert_mismatch(parser, "Test: BAlice", "person") # Similar1. self.assert_mismatch(parser, "Test: Boby", "person") # Similar2. self.assert_mismatch(parser, "Test: a", "person") # INVALID ... # ----------------------------------------------------------------------------- # MAIN: # ----------------------------------------------------------------------------- if __name__ == '__main__': unittest.main() # Copyright (c) 2012-2013 by Jens Engel (https://github/jenisys/parse_type) # # 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. parse_type-0.6.2/tests/test_parse_number.py000066400000000000000000000034521445072161600211410ustar00rootroot00000000000000# -*- coding: UTF-8 -*- """ Additional unit tests for the :mod`parse` module. Related to auto-detection of number base (base=10, 2, 8, 16). """ from __future__ import absolute_import, print_function import pytest import parse parse_version = parse.__version__ print("USING: parse-%s" % parse_version) if parse_version in ("1.17.0", "1.16.0"): # -- REQUIRES: parse >= 1.18.0 -- WORKAROUND HERE print("USING: parse_type.parse (INSTEAD)") from parse_type import parse def assert_parse_number_with_format_d(text, expected): parser = parse.Parser("{value:d}") result = parser.parse(text) assert result.named == dict(value=expected) @pytest.mark.parametrize("text, expected", [ ("123", 123) ]) def test_parse_number_with_base10(text, expected): assert_parse_number_with_format_d(text, expected) @pytest.mark.parametrize("text, expected", [ ("0b0", 0), ("0b1011", 11), ]) def test_parse_number_with_base2(text, expected): assert_parse_number_with_format_d(text, expected) @pytest.mark.parametrize("text, expected", [ ("0o0", 0), ("0o10", 8), ("0o12", 10), ]) def test_parse_number_with_base8(text, expected): assert_parse_number_with_format_d(text, expected) @pytest.mark.parametrize("text, expected", [ ("0x0", 0), ("0x01", 1), ("0x12", 18), ]) def test_parse_number_with_base16(text, expected): assert_parse_number_with_format_d(text, expected) @pytest.mark.parametrize("text1, expected1, text2, expected2", [ ("0x12", 18, "12", 12) ]) def test_parse_number_twice(text1, expected1, text2, expected2): """ENSURE: Issue #121 int_convert memory effect is fixed.""" parser = parse.Parser("{:d}") result1 = parser.parse(text1) result2 = parser.parse(text2) assert result1.fixed[0] == expected1 assert result2.fixed[0] == expected2 parse_type-0.6.2/tests/test_parse_util.py000066400000000000000000000434631445072161600206340ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- """ Test suite to test the :mod:`parse_type.parse_util` module. """ from __future__ import absolute_import, print_function from .parse_type_test import TestCase, unittest from parse_type.parse_util \ import Field, FieldParser, FormatSpec, make_format_spec # ----------------------------------------------------------------------------- # TEST CASE: # ----------------------------------------------------------------------------- class TestField(TestCase): EMPTY_FORMAT_FIELDS = [ Field(), #< Empty field. Field("name"), #< Named field without format. Field("name", ""), #< Named field with format=empty-string. Field(format=""), #< Field with format=empty-string. ] NONEMPTY_FORMAT_FIELDS = [ Field(format="Number"), #< Typed field without name". Field("name", "Number"), #< Named and typed field". ] INVALID_FORMAT_FIELDS = [ Field(format="<"), #< Align without type. Field(format="_<"), #< Fill and align without type. Field(format="_<10"), #< Fill, align and width without type. Field(format="_<098"), #< Fill, align, zero and width without type. ] FIELDS = EMPTY_FORMAT_FIELDS + NONEMPTY_FORMAT_FIELDS + INVALID_FORMAT_FIELDS def test_is_typed__returns_true_for_nonempty_format(self): fields = self.NONEMPTY_FORMAT_FIELDS + self.INVALID_FORMAT_FIELDS for field in fields: self.assertTrue(field.has_format, "Field: %s" % field) def test_is_typed__returns_false_for_empty_format(self): fields = self.EMPTY_FORMAT_FIELDS for field in fields: self.assertFalse(field.has_format, "Field: %s" % field) def test_format_spec__returns_none_if_format_is_empty(self): for field in self.EMPTY_FORMAT_FIELDS: self.assertIsNone(field.format_spec, "Field: %s" % field) def test_format_spec__if_format_is_nonempty_and_valid(self): for field in self.NONEMPTY_FORMAT_FIELDS: self.assertIsNotNone(field.format_spec) self.assertIsInstance(field.format_spec, FormatSpec) def test_format_spec__raises_error_if_nonempty_format_is_invalid(self): for field in self.INVALID_FORMAT_FIELDS: with self.assertRaises(ValueError): field.format_spec def test_format_spec__is_lazy_evaluated(self): fields = [Field(), Field("name"), Field("name", "type"), Field(format="type")] for field in fields: self.assertIsNone(field._format_spec) if field.format: _ = field.format_spec.type self.assertIsNotNone(field.format_spec) else: self.assertIsNone(field.format_spec) def test_set_format_invalidates_format_spec(self): field = Field(format="Number") self.assertEqual(field.format, "Number") self.assertEqual(field.format_spec.type, "Number") self.assertEqual(field.format_spec.align, None) field.set_format("d", "=Number", "^Number+"] for format in formats: format_spec = Field.extract_format_spec(format) expected_align = format[0] expected_type = format[1:] expected_spec = make_format_spec(type=expected_type, align=expected_align) self.assertEqual(format_spec, expected_spec) self.assertValidFormatAlign(format_spec.align) def test_extract_format_spec__with_fill_align_and_type(self): # -- ALIGN_CHARS = "<>=^" formats = ["Xd", "0=Number", " ^Number+"] for format in formats: format_spec = Field.extract_format_spec(format) expected_fill = format[0] expected_align = format[1] expected_type = format[2:] expected_spec = make_format_spec(type=expected_type, align=expected_align, fill=expected_fill) self.assertEqual(format_spec, expected_spec) self.assertValidFormatAlign(format_spec.align) # -- ALIGN_CHARS = "<>=^" FORMAT_AND_FORMAT_SPEC_DATA = [ ("^010Number+", make_format_spec(type="Number+", width="10", zero=True, align="^", fill=None)), ("X<010Number+", make_format_spec(type="Number+", width="10", zero=True, align="<", fill="X")), ("_>0098Number?", make_format_spec(type="Number?", width="098", zero=True, align=">", fill="_")), ("*=129Number*", make_format_spec(type="Number*", width="129", zero=False, align="=", fill="*")), ("X129Number?", make_format_spec(type="X129Number?", width="", zero=False, align=None, fill=None)), (".3Number", make_format_spec(type="Number", width="", zero=False, align=None, fill=None, precision="3")), ("6.2Number", make_format_spec(type="Number", width="6", zero=False, align=None, fill=None, precision="2")), ] def test_extract_format_spec__with_all(self): for format, expected_spec in self.FORMAT_AND_FORMAT_SPEC_DATA: format_spec = Field.extract_format_spec(format) self.assertEqual(format_spec, expected_spec) self.assertValidFormatWidth(format_spec.width) if format_spec.align is not None: self.assertValidFormatAlign(format_spec.align) def test_make_format(self): for expected_format, format_spec in self.FORMAT_AND_FORMAT_SPEC_DATA: format = Field.make_format(format_spec) self.assertEqual(format, expected_format) format_spec2 = Field.extract_format_spec(format) self.assertEqual(format_spec2, format_spec) # ----------------------------------------------------------------------------- # TEST CASE: # ----------------------------------------------------------------------------- class TestFieldParser(TestCase): INVALID_FIELDS = ["", "{", "}", "xxx", "name:type", ":type"] VALID_FIELD_DATA = [ ("{}", Field()), ("{name}", Field("name")), ("{:type}", Field(format="type")), ("{name:type}", Field("name", "type")) ] #def assertFieldEqual(self, actual, expected): # message = "FAILED: %s == %s" % (actual, expected) # self.assertIsInstance(actual, Field) # self.assertIsInstance(expected, Field) # self.assertEqual(actual, expected, message) # # self.assertEqual(actual.name, expected.name, message) # # self.assertEqual(actual.format, expected.format, message) def test_parse__raises_error_with_missing_or_partial_braces(self): for field_text in self.INVALID_FIELDS: with self.assertRaises(ValueError): FieldParser.parse(field_text) def test_parse__with_valid_fields(self): for field_text, expected_field in self.VALID_FIELD_DATA: field = FieldParser.parse(field_text) self.assertEqual(field, expected_field) def test_extract_fields__without_field(self): prefix = "XXX ___" suffix = "XXX {{escaped_field}} {{escaped_field:xxx_type}} XXX" field_texts = [prefix, suffix, prefix + suffix, suffix + prefix] for field_text in field_texts: fields = list(FieldParser.extract_fields(field_text)) self.assertEqual(len(fields), 0) def test_extract_fields__with_one_field(self): prefix = "XXX ___" suffix = "XXX {{escaped_field}} {{escaped_field:xxx_type}} XXX" for field_text, expected_field in self.VALID_FIELD_DATA: fields = list(FieldParser.extract_fields(field_text)) self.assertEqual(len(fields), 1) self.assertSequenceEqual(fields, [expected_field]) field_text2 = prefix + field_text + suffix fields2 = list(FieldParser.extract_fields(field_text2)) self.assertEqual(len(fields2), 1) self.assertSequenceEqual(fields, fields2) def test_extract_fields__with_many_fields(self): MANY_FIELDS_DATA = [ ("{}xxx{name2}", [Field(), Field("name2")]), ("{name1}yyy{:type2}", [Field("name1"), Field(format="type2")]), ("{:type1}xxx{name2}{name3:type3}", [Field(format="type1"), Field("name2"), Field("name3", "type3")]), ] prefix = "XXX ___" suffix = "XXX {{escaped_field}} {{escaped_field:xxx_type}} XXX" for field_text, expected_fields in MANY_FIELDS_DATA: fields = list(FieldParser.extract_fields(field_text)) self.assertEqual(len(fields), len(expected_fields)) self.assertSequenceEqual(fields, expected_fields) field_text2 = prefix + field_text + suffix fields2 = list(FieldParser.extract_fields(field_text2)) self.assertEqual(len(fields2), len(expected_fields)) self.assertSequenceEqual(fields2, expected_fields) def test_extract_types(self): MANY_TYPES_DATA = [ ("{}xxx{name2}", []), ("{name1}yyy{:type2}", ["type2"]), ("{:type1}xxx{name2}{name3:type3}", ["type1", "type3"]), ] for field_text, expected_types in MANY_TYPES_DATA: type_names = list(FieldParser.extract_types(field_text)) self.assertEqual(len(type_names), len(expected_types)) self.assertSequenceEqual(type_names, expected_types) # ----------------------------------------------------------------------------- # MAIN: # ----------------------------------------------------------------------------- if __name__ == '__main__': unittest.main() # Copyright (c) 2012-2013 by Jens Engel (https://github/jenisys/parse_type) # # 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. parse_type-0.6.2/tox.ini000066400000000000000000000066331445072161600152230ustar00rootroot00000000000000# ============================================================================ # TOX CONFIGURATION: parse_type # ============================================================================ # DESCRIPTION: # Use tox to run tasks (tests, ...) in a clean virtual environment. # Tox is configured by default for online usage. # # Run tox, like: # # tox -e py27 # Runs tox with python 2.7 # tox -e py39 # Runs tox with python 3.9 # tox # Runs tox with all installed python versions. # tox --parallel # Runs tox in parallel mode w/ all envs. # # SEE ALSO: # * https://tox.readthedocs.io/en/latest/config.html # ============================================================================ # -- ONLINE USAGE: # PIP_INDEX_URL = https://pypi.org/simple [tox] minversion = 3.10.0 envlist = py311, py310, py39, py38, doctest, pypy3 skip_missing_interpreters = True # DISABLED: sitepackages = False # ----------------------------------------------------------------------------- # TEST ENVIRONMENTS: # ----------------------------------------------------------------------------- # install_command = pip install -U {opts} {packages} [testenv] install_command = pip install -U {opts} {packages} changedir = {toxinidir} commands = pytest {posargs:tests} deps = -r py.requirements/basic.txt -r py.requirements/testing.txt setenv = PYTHONPATH={toxinidir} TOXRUN = yes PYSETUP_BOOTSTRAP = no # -- SPECIAL CASE: # RELATED: https://github.com/pypa/virtualenv/issues/2284 -- macOS 12 Monterey related # NOTES: # * pip-install seems to need "--user" option. # * Script(s) do not seem to be installed any more (actually to $HOME/User area). [testenv:py27] install_command = pip install --user -U {opts} {packages} changedir = {toxinidir} commands= python -m pytest {posargs:tests} deps= {[testenv]deps} passenv = PYTHONPATH = {toxinidir} # MAYBE: allowlist_externals = curl # -- VIRTUAL-ENVIRONMENT SETUP PROCEDURE: For python 2.7 # virtualenv -p python2.7 .venv_py27 # source .venv_py27 # scripts/ensurepip_python27.sh # python -m pip install -r py.requirements/basic.txt # python -m pip install -r py.requirements/testing.txt [testenv:doctest] basepython = python3 commands = pytest --doctest-modules -v parse_type setenv = PYTHONPATH={toxinidir} # ----------------------------------------------------------------------------- # MORE TEST ENVIRONMENTS: # ----------------------------------------------------------------------------- [testenv:coverage] basepython = python3 commands = pytest --cov=parse_type {posargs:tests} coverage combine coverage html coverage xml deps = {[testenv]deps} pytest-cov coverage>=4.0 setenv = PYTHONPATH={toxinidir} [testenv:install] basepython = python3 changedir = {envdir} commands = python ../../setup.py install -q {toxinidir}/bin/toxcmd.py copytree ../../tests . pytest {posargs:tests} deps = {[testenv]deps} setenv = PYTHONPATH={toxinidir} # ----------------------------------------------------------------------------- # SELDOM USED TEST ENVIRONMENTS: # ----------------------------------------------------------------------------- # -- ENSURE: README.rst is well-formed. # python setup.py --long-description | rst2html.py >output.html [testenv:check_setup] changedir = {toxinidir} commands= python setup.py --long-description > output.tmp rst2html.py output.tmp output.html deps = docutils