pax_global_header00006660000000000000000000000064122360245310014510gustar00rootroot0000000000000052 comment=bd940f74275e37680284fe2fb044d12953b68ffb parse_type-0.3.4/000077500000000000000000000000001223602453100136675ustar00rootroot00000000000000parse_type-0.3.4/.bumpversion.cfg000066400000000000000000000001711223602453100167760ustar00rootroot00000000000000[bumpversion] current_version = 0.3.4 files = setup.py parse_type/__init__.py .bumpversion.cfg commit = True tag = True parse_type-0.3.4/.coveragerc000066400000000000000000000016041223602453100160110ustar00rootroot00000000000000# ========================================================================= # 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 = False omit = mock.py, ez_setup.py, distribute.py [report] ignore_errors = False show_missing = True exclude_lines = # pragma: no cover # pragma: recursive coverage def __repr__ raise AssertionError raise NotImplementedError if 0: if False: if __name__ == .__main__.: [html] directory = build/coverage.html title = Coverage Report: parse_type [xml] outfile = build/coverage.xml parse_type-0.3.4/.editorconfig000066400000000000000000000010641223602453100163450ustar00rootroot00000000000000# ============================================================================= # 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.3.4/.gitignore000066400000000000000000000005361223602453100156630ustar00rootroot00000000000000*.py[cod] # C extensions *.so # Packages MANIFEST *.egg *.egg-info dist build downloads eggs parts bin var sdist develop-eggs .installed.cfg lib lib64 __pycache__ # Installer logs pip-log.txt # Unit test / coverage reports .cache/ .idea/ .tox/ .coverage nosetests.xml # Translations *.mo # Mr Developer .mr.developer.cfg .project .pydevproject parse_type-0.3.4/.travis.yml000066400000000000000000000003401223602453100157750ustar00rootroot00000000000000language: python python: - 2.6 - 2.7 - 3.2 - 3.3 - pypy install: - if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install --use-mirrors unittest2; fi - python setup.py -q install script: - py.test tests parse_type-0.3.4/LICENSE000066400000000000000000000027131223602453100146770ustar00rootroot00000000000000Copyright (c) 2013, jenisys All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. Neither the name of the {organization} nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. parse_type-0.3.4/MANIFEST.in000066400000000000000000000005061223602453100154260ustar00rootroot00000000000000include README.rst include LICENSE include .coveragerc include .editorconfig include *.py include *.rst include *.txt include *.ini include *.cfg recursive-include bin *.sh *.py recursive-include docs *.rst *.txt *.py recursive-include requirements *.txt recursive-include tests *.py prune .tox parse_type-0.3.4/README.rst000066400000000000000000000223431223602453100153620ustar00rootroot00000000000000=============================================================================== parse_type =============================================================================== .. image:: https://pypip.in/v/parse_type/badge.png :target: https://crate.io/packages/parse_type/ :alt: Latest PyPI version .. image:: https://pypip.in/d/parse_type/badge.png :target: https://crate.io/packages/parse_type/ :alt: Number of PyPI downloads .. image:: https://travis-ci.org/jenisys/parse_type.png?branch=master :target: https://travis-ci.org/jenisys/parse_type :alt: Travis CI Build Status `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.3.4/_paver_ext/000077500000000000000000000000001223602453100160235ustar00rootroot00000000000000parse_type-0.3.4/_paver_ext/__init__.py000066400000000000000000000000271223602453100201330ustar00rootroot00000000000000# -*- coding: utf-8 -*-parse_type-0.3.4/_paver_ext/paver_consume_args.py000066400000000000000000000023641223602453100222640ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ """ class Cmdline(object): """ Simplify to process consuming args of a task w/o specifying the command line. Splits up into options and args. SPECIFICATION: * An option starts with a dash ('-') or dashdash ('--'). * If an option has a value long options should be used, like: --my-long-option=value EXAMPLE: @task @consume_args def hello(args): cmdline = Cmdline.consume(args, default_args=options.default_args) ... """ def __init__(self, args=None, options=None): self.args = args or [] self.options = options or [] def join_args(self, separator=" "): return separator.join(self.args) def join_options(self, separator=" "): return separator.join(self.options) @classmethod def consume(cls, args, default_args=None, default_options=None): args_ = [] options_ = [] for arg in args: if arg.startswith("-"): options_.append(arg) else: args_.append(arg) if not args_: args_ = default_args if not options_: options_ = default_options return cls(args_, options_) parse_type-0.3.4/_paver_ext/paver_patch.py000066400000000000000000000064701223602453100207000ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ============================================================================ # PAVER EXTENSION: paver_patch # ============================================================================ """ Provide some patch functionality to decouple "pavement.py" from concrete installed paver version. """ from paver.easy import info # ----------------------------------------------------------------------------- # PATCH: PMETHODS, ala path.xxx_p() # ----------------------------------------------------------------------------- _PATH_PMETHOD_NAMES = [ 'makedirs_p', 'mkdir_p', 'remove_p', 'removedirs_p', 'rmdir_p', 'rmtree_p', 'unlink_p', ] def ensure_path_with_pmethods(path_class): """ Adds "..._p()" methods to path class. Earlier versions did not have those. EXAMPLE: # -- file:pavement.py from paver.easy import * sys.path.insert(0, ".") from paver_ext import paver_patch paver_patch.ensure_path_with_pmethods(path) :since: paver.path >= 1.2 """ for pmethod_name in _PATH_PMETHOD_NAMES: assert pmethod_name.endswith("_p") method_name = pmethod_name[:-2] pfunc = getattr(path_class, pmethod_name, None) if not pfunc: # -- PATCH PATH-CLASS: Set path_class.pmethod = method # NOTE: Old method had sanity check that is now provided by pmethod. info("PATCH: path.%s = %s" % (pmethod_name, method_name)) func = getattr(path_class, method_name) setattr(path_class, pmethod_name, func) # ----------------------------------------------------------------------------- # PATCH: SMETHODS (silent methods), ala path.xxx_s() # ----------------------------------------------------------------------------- _PATH_SMETHOD_CREATE_NAMES = [ 'makedirs_s', 'mkdir_s', ] _PATH_SMETHOD_DESTROY_NAMES = [ 'remove_s', 'unlink_s', 'removedirs_s', 'rmdir_s', 'rmtree_s', ] def ensure_path_with_smethods(path_class): """ Adds/patches silent path methods to path class, ala "..._s()". They are executed (and printed) only if something needs to be done. EXAMPLE: # -- file:pavement.py from paver.easy import * sys.path.insert(0, ".") from paver_ext import paver_patch paver_patch.ensure_path_with_smethods(path) .. note:: This is similar to Paver < 1.2 behaviour. """ def _make_screate_func(func): def wrapped(self, *args, **kwargs): if not self.exists(): return func(self, *args, **kwargs) return wrapped def _make_sdestroy_func(func): def wrapped(self, *args, **kwargs): if self.exists(): return func(self, *args, **kwargs) return wrapped smethod_names = _PATH_SMETHOD_CREATE_NAMES + _PATH_SMETHOD_DESTROY_NAMES for smethod_name in smethod_names: assert smethod_name.endswith("_s") pmethod_name = "%s_p" % smethod_name[:-2] func = getattr(path_class, pmethod_name) if smethod_name in _PATH_SMETHOD_CREATE_NAMES: screate_func = _make_screate_func(func) setattr(path_class, smethod_name, screate_func) else: sdestroy_func = _make_sdestroy_func(func) setattr(path_class, smethod_name, sdestroy_func) parse_type-0.3.4/_paver_ext/paver_require.py000066400000000000000000000021631223602453100212500ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ============================================================================ # PAVER EXTENSION: paver_require # ============================================================================ """ Ensure that "pavement.py" are run with the expected paver version. EXAMPLE: # -- file:pavement.py from paver.easy import * sys.path.insert(0, ".") from paver_ext import paver_require paver_require.min_version("1.2") """ from paver.easy import error from paver.release import VERSION as paver_version import sys def min_version(min_version): """ Utility function to ensure that a minimal paver version is used. Aborts paver execution if expectation is not met. :param min_version: Minimum paver version that is required (as string). """ if not (paver_version >= min_version): error("REQUIRE: paver >= %s (actually: %s)" % (min_version, paver_version)) error("ABORT: Here.") sys.exit(1) def assert_min_version(min_version): assert paver_version >= min_version, \ "REQUIRE: paver >= %s (actually: %s)" % (min_version, paver_version) parse_type-0.3.4/_paver_ext/pip_download.py000066400000000000000000000065671223602453100210720ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ============================================================================ # PAVER EXTENSION: Download dependencies with pip via requirements files # ============================================================================ """ A paver extension that provides pip related tasks: - download dependent packages - build a local packages index for downloaded packages EXPECTED OPTIONS STRUCTURE: options.pip .requirements_files -- List of requirements files to use. .download_dir -- Directory for downloaded packages. REQUIRES: * paver >= 1.2 * pip >= 1.1 * bin/make_localpi.py (script) SEE ALSO: * http://www.blueskyonmars.com/projects/paver/ * http://pypi.python.org/pypi/Paver/ * http://pypi.python.org/pypi/pip/ """ from paver.easy import path, sh, task, call_task, consume_args from paver.easy import info, options, BuildFailure # ---------------------------------------------------------------------------- # CONSTANTS: # ---------------------------------------------------------------------------- HERE = path(__file__).dirname() MAKE_LOCALPI_SCRIPT = HERE.joinpath("..", "bin", "make_localpi.py").normpath() # ---------------------------------------------------------------------------- # TASKS: # ---------------------------------------------------------------------------- @task @consume_args def download_deps(args): """Download all dependencies (python packages) with pip.""" download_dir = options.pip.get("download_dir", "$HOME/.pip/downloads") requirements_files = None if args: info("DOWNLOAD DEPENDENCIES: %s" % ", ".join(args)) else: info("DOWNLOAD ALL DEPENDENCIES: %s/" % download_dir) requirements_files = options.pip.requirements_files pip_download(download_dir, args=args, requirements_files=requirements_files) call_task("localpi") @task def localpi(): """Make local python package index from download_dir contents.""" download_dir = path(options.pip.download_dir) if not download_dir.exists(): call_task("download_depends") require_script(MAKE_LOCALPI_SCRIPT) info("MAKE LOCAL PACKAGE-INDEX: %s/" % download_dir) sh("%s %s" % (MAKE_LOCALPI_SCRIPT, download_dir)) # ---------------------------------------------------------------------------- # UTILS: # ---------------------------------------------------------------------------- def require_script(script_path): script_path = path(script_path) if not script_path.exists(): message = "REQUIRE: '%s' => NOT-FOUND" % script_path raise BuildFailure(message) def pip_download(download_dir, cmdopts="", args=None, requirements_files=None): """Download all dependencies with pip by using requirement files, etc.""" requirements = [] if args: requirements.extend(args) if requirements_files: requirements.extend([ "-r %s" % f for f in requirements_files ]) assert requirements, "No requirements provided." # -- NORMAL-CASE: # NOTE: --exists-action option requires pip >= 1.1 download_dir = path(download_dir) download_dir.makedirs_p() pip_download_cmd = "pip install --use-mirrors --exists-action=i" pip_download_cmd += " --download=%s" % download_dir for requirement in requirements: # sh("{pip_download} {cmdopts} {requirement}".format( sh("%s %s %s" % (pip_download_cmd, cmdopts, requirement)) parse_type-0.3.4/_paver_ext/python_bundle.py000066400000000000000000000071501223602453100212520ustar00rootroot00000000000000# ============================================================================ # PAVER EXTENSION (pavement.py) # ============================================================================ # REQUIRES: paver >= 1.0 # DESCRIPTION: # Provides some tasks to bundle python packages. # # SEE ALSO: # * http://pypi.python.org/pypi/Paver/ # * http://www.blueskyonmars.com/projects/paver/ # ============================================================================ from paver.easy import path, task, options, debug, info, error import os # ---------------------------------------------------------------------------- # CONFIGURATION: # ---------------------------------------------------------------------------- #options( # bundle=Bunch( # archive = "python-bundle.zip", # packages = [ "sphinx", "jinja2" ] # ), #) # ---------------------------------------------------------------------------- # TASKS: # ---------------------------------------------------------------------------- @task def bundle(): """Bundle required Python packages as ZIP archive.""" packages = options.bundle.packages builddir = path("build")/"bundle" builddir.makedirs() errors = 0 for package in packages: try: info("bundle %s ..." % package) module = __import__(package) pkgfile = path(module.__file__) basedir = pkgfile.dirname() destdir = builddir/package if destdir.exists(): destdir.rmtree() basedir.copytree(destdir) except StandardError, e: info("FAILED %s: %s" % (package, e)) errors += 1 archive = options.bundle.get("archive", "python-bundle.zip") stored_files = make_zip_archive(archive, builddir) # -- SUMMARY: message1 = "bundle {pkgno} packages into {archive} " message1 += "(with {errorno} errors, files: {fileno})" message = message1.format(archive=archive, pkgno=len(packages), errorno=errors, fileno=stored_files) output = info if errors: message = "FAILED: {message}".format(message=message) output = error output(message) # ---------------------------------------------------------------------------- # UTILS: # ---------------------------------------------------------------------------- def make_zip_archive(archive_name, basedir, files=None, file_pattern="*"): import zipfile archive_name = path(archive_name).abspath() curdir = os.getcwd() os.chdir(basedir) files_count = 0 try: # -- STEP: Collect files. files2 = [] if files is None: dirs = path(".").listdir(pattern="*") files2.extend(dirs) else: for file_ in files: if "*" in file_: parts = path(".").glob(pattern=file_) parts = [ part.normpath() for part in parts ] files2.extend(parts) else: file_ = path(".")/file_ files2.append(file_.normpath()) files3 = [] for file_ in files2: if file_.isdir(): files3.extend(file_.walkfiles(file_pattern)) else: files3.append(file_) files = files3 # -- STEP: Store files in archive. archive = zipfile.ZipFile(archive_name, "w", zipfile.ZIP_DEFLATED) for filename in files: debug("ZIP: Store %s ..." % filename) archive.write(filename) files_count = len(archive.namelist()) archive.close() finally: os.chdir(curdir) return files_count parse_type-0.3.4/_paver_ext/python_checker.py000066400000000000000000000052201223602453100214010ustar00rootroot00000000000000# -*- coding: utf-8 -*- # ============================================================================ # PAVER EXTENSION: Tasks for pychecker, pylint, ... # ============================================================================ """ A paver extension that provides pip related tasks: - download dependent packages - build a local packages index for downloaded packages EXPECTED OPTIONS STRUCTURE: options.pychecker .default_args -- Default args to use (as string). options.pylint .default_args -- Default args to use (as string). REQUIRES: * paver >= 1.0.4 * pychecker >= 0.8.18 * pylint >= 0.25 SEE ALSO: * http://www.blueskyonmars.com/projects/paver/ * http://pypi.python.org/pypi/Paver/ * http://pypi.python.org/pypi/pylint/ * http://pypi.python.org/pypi/pychecker/ """ from __future__ import with_statement from paver.easy import consume_args, error, info, options, path, sh, task from paver.easy import call_task # ---------------------------------------------------------------------------- # TASKS: # ---------------------------------------------------------------------------- @task @consume_args def pychecker(args): """Run pychecker on sources.""" if not args: args = options.pychecker.default_args.split() # -- COLLECT: command options, files problematic = [] cmdopts = [] files = [] for arg in args: path_ = path(arg) if arg.startswith("-"): cmdopts.append(arg) elif path_.isdir(): files.extend(path_.walkfiles("*.py")) elif arg.endswith(".py") and path_.exists(): files.append(arg) else: error("UNKNOWN FILE: {0}".format(arg)) problematic.append(arg) # -- EXECUTE: cmdopts = " ".join(cmdopts) for file_ in files: try: sh("pychecker {opts} {file}".format(opts=cmdopts, file=file_)) except Exception as e: error("FAILURE: {0}".format(e)) problematic.append(file_) # -- SUMMARY: if problematic: errors = len(problematic) error("PYCHECKER FAILED: {0} error(s) occured.".format(errors)) error("PROBLEMATIC:") for file_ in problematic: error(" - {0}".format(file_)) else: info("PYCHECKER SUCCESS: {0} file(s).".format(len(files))) @task @consume_args def pylint(args): """Run pylint on sources.""" if not args: args = options.pychecker.default_args.split() cmdline = " ".join(args) sh("pylint %s" % cmdline) @task @consume_args def all_checkers(args): """Run all python checkers.""" call_task("pychecker") call_task("pylint") parse_type-0.3.4/_paver_ext/python_requirements.py000066400000000000000000000036721223602453100225310ustar00rootroot00000000000000# ============================================================================ # PAVER EXTENSION/UTILITY: Read PIP requirements files # ============================================================================ # REQUIRES: paver >= 1.0 # REQUIRES: pkg_resources, fulfilled when setuptools or distribute is installed # DESCRIPTION: # Provides some utility functions for paver. # # SEE ALSO: # * http://pypi.python.org/pypi/Paver/ # * http://www.blueskyonmars.com/projects/paver/ # ============================================================================ # from paver.easy import error import os.path import pkg_resources import sys def error(text): sys.stderr.write("ERROR: %s\n" % text) sys.stderr.flush() # ---------------------------------------------------------------------------- # UTILS: # ---------------------------------------------------------------------------- def read_requirements(*filenames): """ Read PIP "requirements*.txt" files. These files contains python package requirements. :param filenames: List of requirement files to read. :returns: List of packages/package requirements (list-of-strings). """ package_requirements = [] for filename in filenames: if not os.path.exists(filename): error("REQUIREMENT-FILE %s not found" % filename) continue # XXX-INSTALL-REQUIRES problem w/ setup() # # -- NORMAL CASE: # with open(filename, "r") as f: # requirements = pkg_resources.parse_requirements(f.read()) # package_requirements.extend(requirements) # -- NORMAL CASE: requirements_file = open(filename, "r") for line in requirements_file.readlines(): line = line.strip() if not line or line.startswith("#"): continue #< SKIP: EMPTY-LINE or COMMENT-LINE package_requirements.append(line) requirements_file.close() return package_requirements parse_type-0.3.4/bin/000077500000000000000000000000001223602453100144375ustar00rootroot00000000000000parse_type-0.3.4/bin/make_localpi.py000077500000000000000000000170151223602453100174400ustar00rootroot00000000000000#!/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.3.4/bin/project_bootstrap.sh000077500000000000000000000012561223602453100205450ustar00rootroot00000000000000#!/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 --use-mirrors --download=${PIP_DOWNLOAD_DIR} -r ${TOP}/requirements/all.txt ${HERE}/make_localpi.py ${PIP_DOWNLOAD_DIR} parse_type-0.3.4/bin/toxcmd.py000077500000000000000000000210721223602453100163140ustar00rootroot00000000000000#!/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.3.4/ez_setup.py000066400000000000000000000270751223602453100161120ustar00rootroot00000000000000#!python """Bootstrap setuptools installation If you want to use setuptools in your package's setup.py, just include this file in the same directory with it, and add this to the top of your setup.py:: from ez_setup import use_setuptools use_setuptools() If you want to require a specific version of setuptools, set a download mirror, or use an alternate download directory, you can do so by supplying the appropriate options to ``use_setuptools()``. This file can also be run as a script to install or upgrade setuptools. """ import os import shutil import sys import tempfile import tarfile import optparse import subprocess import platform from distutils import log try: from site import USER_SITE except ImportError: USER_SITE = None DEFAULT_VERSION = "1.1.6" DEFAULT_URL = "https://pypi.python.org/packages/source/s/setuptools/" def _python_cmd(*args): args = (sys.executable,) + args return subprocess.call(args) == 0 def _check_call_py24(cmd, *args, **kwargs): res = subprocess.call(cmd, *args, **kwargs) class CalledProcessError(Exception): pass if not res == 0: msg = "Command '%s' return non-zero exit status %d" % (cmd, res) raise CalledProcessError(msg) vars(subprocess).setdefault('check_call', _check_call_py24) def _install(tarball, install_args=()): # extracting the tarball tmpdir = tempfile.mkdtemp() log.warn('Extracting in %s', tmpdir) old_wd = os.getcwd() try: os.chdir(tmpdir) tar = tarfile.open(tarball) _extractall(tar) tar.close() # going in the directory subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) os.chdir(subdir) log.warn('Now working in %s', subdir) # installing log.warn('Installing Setuptools') if not _python_cmd('setup.py', 'install', *install_args): log.warn('Something went wrong during the installation.') log.warn('See the error message above.') # exitcode will be 2 return 2 finally: os.chdir(old_wd) shutil.rmtree(tmpdir) def _build_egg(egg, tarball, to_dir): # extracting the tarball tmpdir = tempfile.mkdtemp() log.warn('Extracting in %s', tmpdir) old_wd = os.getcwd() try: os.chdir(tmpdir) tar = tarfile.open(tarball) _extractall(tar) tar.close() # going in the directory subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) os.chdir(subdir) log.warn('Now working in %s', subdir) # building an egg log.warn('Building a Setuptools egg in %s', to_dir) _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir) finally: os.chdir(old_wd) shutil.rmtree(tmpdir) # returning the result log.warn(egg) if not os.path.exists(egg): raise IOError('Could not build the egg.') def _do_download(version, download_base, to_dir, download_delay): egg = os.path.join(to_dir, 'setuptools-%s-py%d.%d.egg' % (version, sys.version_info[0], sys.version_info[1])) if not os.path.exists(egg): tarball = download_setuptools(version, download_base, to_dir, download_delay) _build_egg(egg, tarball, to_dir) sys.path.insert(0, egg) # Remove previously-imported pkg_resources if present (see # https://bitbucket.org/pypa/setuptools/pull-request/7/ for details). if 'pkg_resources' in sys.modules: del sys.modules['pkg_resources'] import setuptools setuptools.bootstrap_install_from = egg def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, download_delay=15): # making sure we use the absolute path to_dir = os.path.abspath(to_dir) was_imported = 'pkg_resources' in sys.modules or \ 'setuptools' in sys.modules try: import pkg_resources except ImportError: return _do_download(version, download_base, to_dir, download_delay) try: pkg_resources.require("setuptools>=" + version) return except pkg_resources.VersionConflict: e = sys.exc_info()[1] if was_imported: sys.stderr.write( "The required version of setuptools (>=%s) is not available,\n" "and can't be installed while this script is running. Please\n" "install a more recent version first, using\n" "'easy_install -U setuptools'." "\n\n(Currently using %r)\n" % (version, e.args[0])) sys.exit(2) else: del pkg_resources, sys.modules['pkg_resources'] # reload ok return _do_download(version, download_base, to_dir, download_delay) except pkg_resources.DistributionNotFound: return _do_download(version, download_base, to_dir, download_delay) def download_file_powershell(url, target): """ Download the file at url to target using Powershell (which will validate trust). Raise an exception if the command cannot complete. """ target = os.path.abspath(target) cmd = [ 'powershell', '-Command', "(new-object System.Net.WebClient).DownloadFile(%(url)r, %(target)r)" % vars(), ] subprocess.check_call(cmd) def has_powershell(): if platform.system() != 'Windows': return False cmd = ['powershell', '-Command', 'echo test'] devnull = open(os.path.devnull, 'wb') try: try: subprocess.check_call(cmd, stdout=devnull, stderr=devnull) except: return False finally: devnull.close() return True download_file_powershell.viable = has_powershell def download_file_curl(url, target): cmd = ['curl', url, '--silent', '--output', target] subprocess.check_call(cmd) def has_curl(): cmd = ['curl', '--version'] devnull = open(os.path.devnull, 'wb') try: try: subprocess.check_call(cmd, stdout=devnull, stderr=devnull) except: return False finally: devnull.close() return True download_file_curl.viable = has_curl def download_file_wget(url, target): cmd = ['wget', url, '--quiet', '--output-document', target] subprocess.check_call(cmd) def has_wget(): cmd = ['wget', '--version'] devnull = open(os.path.devnull, 'wb') try: try: subprocess.check_call(cmd, stdout=devnull, stderr=devnull) except: return False finally: devnull.close() return True download_file_wget.viable = has_wget def download_file_insecure(url, target): """ Use Python to download the file, even though it cannot authenticate the connection. """ try: from urllib.request import urlopen except ImportError: from urllib2 import urlopen src = dst = None try: src = urlopen(url) # Read/write all in one block, so we don't create a corrupt file # if the download is interrupted. data = src.read() dst = open(target, "wb") dst.write(data) finally: if src: src.close() if dst: dst.close() download_file_insecure.viable = lambda: True def get_best_downloader(): downloaders = [ download_file_powershell, download_file_curl, download_file_wget, download_file_insecure, ] for dl in downloaders: if dl.viable(): return dl def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, delay=15, downloader_factory=get_best_downloader): """Download setuptools from a specified location and return its filename `version` should be a valid setuptools version number that is available as an egg for download under the `download_base` URL (which should end with a '/'). `to_dir` is the directory where the egg will be downloaded. `delay` is the number of seconds to pause before an actual download attempt. ``downloader_factory`` should be a function taking no arguments and returning a function for downloading a URL to a target. """ # making sure we use the absolute path to_dir = os.path.abspath(to_dir) tgz_name = "setuptools-%s.tar.gz" % version url = download_base + tgz_name saveto = os.path.join(to_dir, tgz_name) if not os.path.exists(saveto): # Avoid repeated downloads log.warn("Downloading %s", url) downloader = downloader_factory() downloader(url, saveto) return os.path.realpath(saveto) def _extractall(self, path=".", members=None): """Extract all members from the archive to the current working directory and set owner, modification time and permissions on directories afterwards. `path' specifies a different directory to extract to. `members' is optional and must be a subset of the list returned by getmembers(). """ import copy import operator from tarfile import ExtractError directories = [] if members is None: members = self for tarinfo in members: if tarinfo.isdir(): # Extract directories with a safe mode. directories.append(tarinfo) tarinfo = copy.copy(tarinfo) tarinfo.mode = 448 # decimal for oct 0700 self.extract(tarinfo, path) # Reverse sort directories. if sys.version_info < (2, 4): def sorter(dir1, dir2): return cmp(dir1.name, dir2.name) directories.sort(sorter) directories.reverse() else: directories.sort(key=operator.attrgetter('name'), reverse=True) # Set correct owner, mtime and filemode on directories. for tarinfo in directories: dirpath = os.path.join(path, tarinfo.name) try: self.chown(tarinfo, dirpath) self.utime(tarinfo, dirpath) self.chmod(tarinfo, dirpath) except ExtractError: e = sys.exc_info()[1] if self.errorlevel > 1: raise else: self._dbg(1, "tarfile: %s" % e) def _build_install_args(options): """ Build the arguments to 'python setup.py install' on the setuptools package """ install_args = [] if options.user_install: if sys.version_info < (2, 6): log.warn("--user requires Python 2.6 or later") raise SystemExit(1) install_args.append('--user') return install_args def _parse_args(): """ Parse the command line for options """ parser = optparse.OptionParser() parser.add_option( '--user', dest='user_install', action='store_true', default=False, help='install in user site package (requires Python 2.6 or later)') parser.add_option( '--download-base', dest='download_base', metavar="URL", default=DEFAULT_URL, help='alternative URL from where to download the setuptools package') parser.add_option( '--insecure', dest='downloader_factory', action='store_const', const=lambda: download_file_insecure, default=get_best_downloader, help='Use internal, non-validating downloader' ) options, args = parser.parse_args() # positional arguments are ignored return options def main(version=DEFAULT_VERSION): """Install or upgrade setuptools and EasyInstall""" options = _parse_args() tarball = download_setuptools(download_base=options.download_base, downloader_factory=options.downloader_factory) return _install(tarball, _build_install_args(options)) if __name__ == '__main__': sys.exit(main()) parse_type-0.3.4/parse_type/000077500000000000000000000000001223602453100160425ustar00rootroot00000000000000parse_type-0.3.4/parse_type/__init__.py000066400000000000000000000026151223602453100201570ustar00rootroot00000000000000# -*- coding: utf-8 -*- from parse_type.cardinality import Cardinality from parse_type.builder import TypeBuilder, build_type_dict __all__ = ["Cardinality", "TypeBuilder", "build_type_dict"] __version__ = "0.3.4" # ----------------------------------------------------------------------------- # Copyright (c) 2012-2013 by Jens Engel (https://github/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.3.4/parse_type/builder.py000066400000000000000000000270711223602453100200510ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ 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 from .cardinality import \ Cardinality, TypeBuilder as CardinalityTypeBuilder from parse_type.cardinality import pattern_group_count import enum import inspect import re __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 not (text 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 not (text 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.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): 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): 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). """ return text parse_anything.pattern = TypeBuilder.anything_pattern # ----------------------------------------------------------------------------- # 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.3.4/parse_type/cardinality.py000066400000000000000000000177201223602453100207260ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ This module simplifies to build parse types and regular expressions for a data type with the specified cardinality. """ # -- USE: enum34 from enum import Enum # ----------------------------------------------------------------------------- # FUNCTIONS: # ----------------------------------------------------------------------------- def pattern_group_count(pattern): 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. """ __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 else: 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) else: # -- 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): if text: text = text.strip() if not text: return None return converter(text) convert_optional.pattern = optional_pattern convert_optional.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): if text: text = text.strip() if not text: return [] return [converter(part.strip()) for part in text.split(listsep)] convert_list0.pattern = many0_pattern convert_list0.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): return [converter(part.strip()) for part in text.split(listsep)] convert_list.pattern = many_pattern convert_list.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.3.4/parse_type/cardinality_field.py000066400000000000000000000153031223602453100220640ustar00rootroot00000000000000# -*- 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 from .cardinality import Cardinality, TypeBuilder import six class MissingTypeError(KeyError): 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 STATUS: IDEA, currently not accepted in :mod:`parse` module. """ # -- 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): """ 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 # XXX-JE-TODO: 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.3.4/parse_type/cfparse.py000066400000000000000000000065521223602453100200470ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Provides an extended :class:`parse.Parser` class that supports the cardinality fields in (user-defined) types. """ from __future__ import absolute_import from .cardinality_field import CardinalityField, CardinalityFieldTypeBuilder from .parse_util import FieldParser import parse import logging log = logging.getLogger(__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={}, 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. :param type_builder: Type builder to use for missing types. """ missing = self.create_missing_types(schema, extra_types, type_builder) if missing: log.debug("MISSING TYPES: %s" % ",".join(missing.keys())) extra_types.update(missing) # -- FINALLY: Delegate to base class. super(Parser, self).__init__(schema, extra_types) @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): """ 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.3.4/parse_type/parse.py000066400000000000000000001136371223602453100175410ustar00rootroot00000000000000# -*- coding: utf-8 -*- # BASED-ON: https://github.com/r1chardj0n3s/parse/parse.py # VERSION: parse 1.6.3 # Same as original parse module except the following extensions. # EXTENSIONS: # * group_count attribute support for user-defined type converters # => FIXES: group_index offset problem for fixed fields # when pattern has groups # -- ORIGINAL-CODE STARTS-HERE ------------------------------------------------ r'''Parse strings using a specification based on the Python format() syntax. ``parse()`` is the opposite of ``format()`` The module is set up to only export ``parse()``, ``search()`` and ``findall()`` when ``import *`` is used: >>> from parse import * From there it's a simple thing to parse a string: >>> parse("It's {}, I love it!", "It's spam, I love it!") >>> _[0] 'spam' Or to search a string for some pattern: >>> search('Age: {:d}\n', 'Name: Rufus\nAge: 42\nColor: red\n') Or find all the occurrances of some pattern in a string: >>> ''.join(r.fixed[0] for r in findall(">{}<", "

    the bold text

    ")) 'the bold text' If you're going to use the same pattern to match lots of strings you can compile it once: >>> from parse import compile >>> p = compile("It's {}, I love it!") >>> print(p) >>> p.parse("It's spam, I love it!") ("compile" is not exported for ``import *`` usage as it would override the built-in ``compile()`` function) Format Syntax ------------- A basic version of the `Format String Syntax`_ is supported with anonymous (fixed-position), named and formatted fields:: {[field name]:[format spec]} Field names must be a valid Python identifiers, including dotted names; element indexes are supported (as they would make no sense.) Numbered fields are also not supported: the result of parsing will include the parsed fields in the order they are parsed. The conversion of fields to types other than strings is done based on the type in the format specification, which mirrors the ``format()`` behaviour. There are no "!" field conversions like ``format()`` has. Some simple parse() format string examples: >>> parse("Bring me a {}", "Bring me a shrubbery") >>> r = parse("The {} who say {}", "The knights who say Ni!") >>> print(r) >>> print(r.fixed) ('knights', 'Ni!') >>> r = parse("Bring out the holy {item}", "Bring out the holy hand grenade") >>> print(r) >>> print(r.named) {'item': 'hand grenade'} >>> print(r['item']) hand grenade Dotted names are possible though the application must make additional sense of the result: >>> r = parse("Mmm, {food.type}, I love it!", "Mmm, spam, I love it!") >>> print(r) >>> print(r.named) {'food.type': 'spam'} >>> print(r['food.type']) spam Format Specification -------------------- Most often a straight format-less ``{}`` will suffice where a more complex format specification might have been used. Most of `format()`'s `Format Specification Mini-Language`_ is supported: [[fill]align][0][width][type] The differences between `parse()` and `format()` are: - The align operators will cause spaces (or specified fill character) to be stripped from the parsed value. The width is not enforced; it just indicates there may be whitespace or "0"s to strip. - Numeric parsing will automatically handle a "0b", "0o" or "0x" prefix. That is, the "#" format character is handled automatically by d, b, o and x formats. For "d" any will be accepted, but for the others the correct prefix must be present if at all. - Numeric sign is handled automatically. - The thousands separator is handled automatically if the "n" type is used. - The types supported are a slightly different mix to the format() types. Some format() types come directly over: "d", "n", "%", "f", "e", "b", "o" and "x". In addition some regular expression character group types "D", "w", "W", "s" and "S" are also available. - The "e" and "g" types are case-insensitive so there is not need for the "E" or "G" types. ===== =========================================== ======== Type Characters Matched Output ===== =========================================== ======== w Letters and underscore str W Non-letter and underscore str s Whitespace str S Non-whitespace str d Digits (effectively integer numbers) int D Non-digit str n Numbers with thousands separators (, or .) int % Percentage (converted to value/100.0) float f Fixed-point numbers float e Floating-point numbers with exponent float e.g. 1.1e-10, NAN (all case insensitive) g General number format (either d, f or e) float b Binary numbers int o Octal numbers int x Hexadecimal numbers (lower and upper case) int ti ISO 8601 format date/time datetime e.g. 1972-01-20T10:21:36Z ("T" and "Z" optional) te RFC2822 e-mail format date/time datetime e.g. Mon, 20 Jan 1972 10:21:36 +1000 tg Global (day/month) format date/time datetime e.g. 20/1/1972 10:21:36 AM +1:00 ta US (month/day) format date/time datetime e.g. 1/20/1972 10:21:36 PM +10:30 tc ctime() format date/time datetime e.g. Sun Sep 16 01:03:52 1973 th HTTP log format date/time datetime e.g. 21/Nov/2011:00:07:11 +0000 tt Time time e.g. 10:21:36 PM -5:30 ===== =========================================== ======== Some examples of typed parsing with ``None`` returned if the typing does not match: >>> parse('Our {:d} {:w} are...', 'Our 3 weapons are...') >>> parse('Our {:d} {:w} are...', 'Our three weapons are...') >>> parse('Meet at {:tg}', 'Meet at 1/2/2011 11:00 PM') And messing about with alignment: >>> parse('with {:>} herring', 'with a herring') >>> parse('spam {:^} spam', 'spam lovely spam') Note that the "center" alignment does not test to make sure the value is centered - it just strips leading and trailing whitespace. Some notes for the date and time types: - the presence of the time part is optional (including ISO 8601, starting at the "T"). A full datetime object will always be returned; the time will be set to 00:00:00. You may also specify a time without seconds. - when a seconds amount is present in the input fractions will be parsed to give microseconds. - except in ISO 8601 the day and month digits may be 0-padded. - the date separator for the tg and ta formats may be "-" or "/". - named months (abbreviations or full names) may be used in the ta and tg formats in place of numeric months. - as per RFC 2822 the e-mail format may omit the day (and comma), and the seconds but nothing else. - hours greater than 12 will be happily accepted. - the AM/PM are optional, and if PM is found then 12 hours will be added to the datetime object's hours amount - even if the hour is greater than 12 (for consistency.) - in ISO 8601 the "Z" (UTC) timezone part may be a numeric offset - timezones are specified as "+HH:MM" or "-HH:MM". The hour may be one or two digits (0-padded is OK.) Also, the ":" is optional. - the timezone is optional in all except the e-mail format (it defaults to UTC.) - named timezones are not handled yet. Note: attempting to match too many datetime fields in a single parse() will currently result in a resource allocation issue. A TooManyFields exception will be raised in this instance. The current limit is about 15. It is hoped that this limit will be removed one day. .. _`Format String Syntax`: http://docs.python.org/library/string.html#format-string-syntax .. _`Format Specification Mini-Language`: http://docs.python.org/library/string.html#format-specification-mini-language Result Objects -------------- The result of a ``parse()`` operation is either ``None`` (no match) or a ``Result`` instance. The ``Result`` instance has three attributes: fixed A tuple of the fixed-position, anonymous fields extracted from the input. named A dictionary of the named fields extracted from the input. spans A dictionary mapping the names and fixed position indices matched to a 2-tuple slice range of where the match occurred in the input. The span does not include any stripped padding (alignment or width). Custom Type Conversions ----------------------- If you wish to have matched fields automatically converted to your own type you may pass in a dictionary of type conversion information to ``parse()`` and ``compile()``. The converter will be passed the field string matched. Whatever it returns will be substituted in the ``Result`` instance for that field. Your custom type conversions may override the builtin types if you supply one with the same identifier. >>> def shouty(string): ... return string.upper() ... >>> parse('{:shouty} world', 'hello world', dict(shouty=shouty)) If the type converter has the optional ``pattern`` attribute, it is used as regular expression for better pattern matching (instead of the default one). >>> def parse_number(text): ... return int(text) >>> parse_number.pattern = r'\d+' >>> parse('Answer: {number:Number}', 'Answer: 42', dict(Number=parse_number)) >>> _ = parse('Answer: {:Number}', 'Answer: Alice', dict(Number=parse_number)) >>> assert _ is None, "MISMATCH" You can also use the ``with_pattern(pattern)`` decorator to add this information to a type converter function: >>> from parse import with_pattern >>> @with_pattern(r'\d+') ... def parse_number(text): ... return int(text) >>> parse('Answer: {number:Number}', 'Answer: 42', dict(Number=parse_number)) A more complete example of a custom type might be: >>> yesno_mapping = { ... "yes": True, "no": False, ... "on": True, "off": False, ... "true": True, "false": False, ... } ... @with_pattern(r"|".join(yesno_mapping)) ... def parse_yesno(text): ... return yesno_mapping[text.lower()] ---- **Version history (in brief)**: - 1.6.3 handle repeated instances of named fields, fix bug in PM time overflow - 1.6.2 fix logging to use local, not root logger (thanks Necku) - 1.6.1 be more flexible regarding matched ISO datetimes and timezones in general, fix bug in timezones without ":" and improve docs - 1.6.0 add support for optional ``pattern`` attribute in user-defined types (thanks Jens Engel) - 1.5.3 fix handling of question marks - 1.5.2 fix type conversion error with dotted names (thanks Sebastian Thiel) - 1.5.1 implement handling of named datetime fields - 1.5 add handling of dotted field names (thanks Sebastian Thiel) - 1.4.1 fix parsing of "0" in int conversion (thanks James Rowe) - 1.4 add __getitem__ convenience access on Result. - 1.3.3 fix Python 2.5 setup.py issue. - 1.3.2 fix Python 3.2 setup.py issue. - 1.3.1 fix a couple of Python 3.2 compatibility issues. - 1.3 added search() and findall(); removed compile() from ``import *`` export as it overwrites builtin. - 1.2 added ability for custom and override type conversions to be provided; some cleanup - 1.1.9 to keep things simpler number sign is handled automatically; significant robustification in the face of edge-case input. - 1.1.8 allow "d" fields to have number base "0x" etc. prefixes; fix up some field type interactions after stress-testing the parser; implement "%" type. - 1.1.7 Python 3 compatibility tweaks (2.5 to 2.7 and 3.2 are supported). - 1.1.6 add "e" and "g" field types; removed redundant "h" and "X"; removed need for explicit "#". - 1.1.5 accept textual dates in more places; Result now holds match span positions. - 1.1.4 fixes to some int type conversion; implemented "=" alignment; added date/time parsing with a variety of formats handled. - 1.1.3 type conversion is automatic based on specified field types. Also added "f" and "n" types. - 1.1.2 refactored, added compile() and limited ``from parse import *`` - 1.1.1 documentation improvements - 1.1.0 implemented more of the `Format Specification Mini-Language`_ and removed the restriction on mixing fixed-position and named fields - 1.0.0 initial release This code is copyright 2012-2013 Richard Jones See the end of the source file for the license of use. ''' __version__ = '1.6.3' # yes, I now have two problems import re import sys from datetime import datetime, time, tzinfo, timedelta from functools import partial import logging __all__ = 'parse search findall with_pattern'.split() log = logging.getLogger(__name__) def with_pattern(pattern): """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) :return: wrapped function """ def decorator(func): func.pattern = pattern return func return decorator def int_convert(base): '''Convert a string to an integer. The string may start with a sign. It may be of a base other than 10. It may also have other non-numeric characters that we can ignore. ''' CHARS = '0123456789abcdefghijklmnopqrstuvwxyz' def f(string, match, base=base): if string[0] == '-': sign = -1 else: sign = 1 if string[0] == '0' and len(string) > 1: if string[1] in 'bB': base = 2 elif string[1] in 'oO': base = 8 elif string[1] in 'xX': base = 16 else: # just go with the base specifed pass chars = CHARS[:base] string = re.sub('[^%s]' % chars, '', string.lower()) return sign * int(string, base) return f def percentage(string, match): return float(string[:-1]) / 100. 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): 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 = '(Mon|Tue|Wed|Thu|Fri|Sat|Sun)' MONTHS_PAT = '(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)' ALL_MONTHS_PAT = '(%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): '''Convert the incoming string containing some date / time info into a datetime instance. ''' groups = match.groups() time_only = False if ymd is not None: y, m, d = re.split('[-/\s]', groups[ymd]) elif mdy is not None: m, d, y = re.split('[-/\s]', groups[mdy]) elif dmy is not None: d, m, y = re.split('[-/\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) day_incr = False if am is not None: am = groups[am] if am and am.strip() == 'PM': H += 12 if H > 23: day_incr = True H -= 24 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) if day_incr: d = d + timedelta(days=1) 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('([?\\\\.[\]()*+\^$!])') # allowed field types ALLOWED_TYPES = set(list('nbox%fegwWdDsS') + ['t' + c for c in 'ieahgct']) def extract_format(format, extra_types): '''Pull apart the format [[fill]align][0][width][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:] 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:] # 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('type %r not recognised' % type) return locals() PARSE_RE = re.compile(r'({{|}}|{}|{:[^}]+?}|{\w+?(?:\.\w+?)*}|' r'{\w+?(?:\.\w+?)*:[^}]+?})') class Parser(object): '''Encapsulate a format string that may be used to parse other strings. ''' def __init__(self, format, extra_types={}): # 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 self._extra_types = extra_types 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, re.IGNORECASE | re.DOTALL) 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 = '^%s$' % self._expression try: self.__match_re = re.compile(expression, re.IGNORECASE | re.DOTALL) 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 def parse(self, string): '''Match my format to the string exactly. Return either a Result instance or None if there's no match. ''' m = self._match_re.match(string) if m is None: return None return self._generate_result(m) def search(self, string, pos=0, endpos=None): '''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]). 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 return self._generate_result(m) def findall(self, string, pos=0, endpos=None, extra_types={}): '''Search "string" for the all occurrances 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 instances for each format match found. ''' if endpos is None: endpos = len(string) return ResultIterator(self, string, pos, endpos) def _generate_result(self, m): # 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: named_fields[korig] = self._type_conversions[k](groupdict[k], m) else: named_fields[korig] = groupdict[k] # 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, 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] == '{': # 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('.', '_') # 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 field and field[0].isalpha(): if ':' in field: name, format = field.split(':') else: name = field 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 '(?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 = '(?P<%s>%%s)' % group else: self._fixed_fields.append(self._group_index) wrap = '(%s)' if ':' in field: format = field[1:] group = self._group_index # simplest case: no type specifier ({} or {name}) if not format: self._group_index += 1 return wrap % '.+?' # 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%fegdobh' if type in self._extra_types: type_converter = self._extra_types[type] s = getattr(type_converter, 'pattern', r'.+?') # -- EXTENSION: group_count attribute group_count = getattr(type_converter, 'group_count', 0) self._group_index += group_count # -- EXTENSION-END def f(string, m): return type_converter(string) self._type_conversions[group] = f elif type == 'n': s = '\d{1,3}([,.]\d{3})*' self._group_index += 1 self._type_conversions[group] = int_convert(10) elif type == 'b': s = '(0[bB])?[01]+' self._type_conversions[group] = int_convert(2) self._group_index += 1 elif type == 'o': s = '(0[oO])?[0-7]+' self._type_conversions[group] = int_convert(8) self._group_index += 1 elif type == 'x': s = '(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] = lambda s, m: float(s) elif type == 'e': s = r'\d+\.\d+[eE][-+]?\d+|nan|NAN|[-+]?inf|[-+]?INF' self._type_conversions[group] = lambda s, m: float(s) elif type == 'g': s = r'\d+(\.\d+)?([eE][-+]?\d+)?|nan|NAN|[-+]?inf|[-+]?INF' self._group_index += 2 self._type_conversions[group] = lambda s, m: float(s) elif type == 'd': s = r'\d+|0[xX][0-9a-fA-F]+|[0-9a-fA-F]+|0[bB][01]+|0[oO][0-7]+' self._type_conversions[group] = int_convert(10) 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: s = r'\%s+' % type else: s = '.+?' 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 = '%s*' % fill + s elif format['zero']: s = '0*' + 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 '.\+?*[](){}^$': 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]. Named results may be looked up using result['name']. ''' def __init__(self, fixed, named, spans): self.fixed = fixed self.named = named self.spans = spans def __getitem__(self, item): if isinstance(item, int): return self.fixed[item] return self.named[item] def __repr__(self): return '<%s %r %r>' % (self.__class__.__name__, self.fixed, self.named) class ResultIterator(object): '''The result of a findall() operation. Each element is a Result instance. ''' def __init__(self, parser, string, pos, endpos): self.parser = parser self.string = string self.pos = pos self.endpos = endpos 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() return self.parser._generate_result(m) # pre-py3k compat next = __next__ def parse(format, string, extra_types={}): '''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(). 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 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. ''' return Parser(format, extra_types=extra_types).parse(string) def search(format, string, pos=0, endpos=None, extra_types={}): '''Search "string" for the first occurance 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]). 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 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. ''' return Parser(format, extra_types=extra_types).search(string, pos, endpos) def findall(format, string, pos=0, endpos=None, extra_types={}): '''Search "string" for the all occurrances 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]). Each Result instance has two attributes: .fixed - tuple of fixed-position values from the string .named - dict of named values from the string If the format is invalid a ValueError will be raised. See the module documentation for the use of "extra_types". ''' return Parser(format, extra_types=extra_types).findall(string, pos, endpos) def compile(format, extra_types={}): '''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). 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) # Copyright (c) 2012-2013 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.3.4/parse_type/parse_util.py000066400000000000000000000121561223602453100205700ustar00rootroot00000000000000# -*- coding: utf-8 -*- """ Provides generic utility classes for the :class:`parse.Parser` class. """ import parse from collections import namedtuple import six # -- HELPER-CLASS: For format part in a Field. # REQUIRES: Python 2.6 or newer. FormatSpec = namedtuple("FormatSpec", ["type", "width", "zero", "align", "fill"]) def make_format_spec(type=None, width="", zero=False, align=None, fill=None): return FormatSpec(type, width, zero, align, fill) class Field(object): """ Provides a ValueObject for a Field in a parse expression. Examples: * "{}" * "{name}" * "{:format}" * "{name:format}" Format specification: [[fill]align][0][width][type] """ 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) else: 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' # -- FORMAT-SPEC: [[fill]align][0][width][type] return "%s%s%s%s%s" % (fill, align, zero, width, format_spec.type) @classmethod def extract_format_spec(cls, format): """Pull apart the format [[fill]align][0][width][type]""" # -- BASED-ON: parse.extract_format() 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:] # 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) 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.3.4/pavement.py000066400000000000000000000141361223602453100160650ustar00rootroot00000000000000# ============================================================================ # PAVER MAKEFILE: pavement.py example # ============================================================================ # REQUIRES: paver >= 1.2 # DESCRIPTION: # Provides platform-neutral "Makefile" for simple, project-specific tasks. # # USAGE: # paver TASK [OPTIONS...] # paver help -- Show all supported commands/tasks. # paver clean -- Cleanup project workspace. # paver test -- Run all tests (unittests, examples). # # SEE ALSO: # * http://pypi.python.org/pypi/Paver/ # * http://www.blueskyonmars.com/projects/paver/ # ============================================================================ from paver.easy import * import os sys.path.insert(0, ".") # -- USE PAVER EXTENSIONS: tasks, utility functions from _paver_ext.pip_download import download_deps, localpi from _paver_ext.python_checker import pychecker, pylint from _paver_ext.python_requirements import read_requirements from _paver_ext.paver_consume_args import Cmdline from _paver_ext import paver_require, paver_patch paver_require.min_version("1.2") paver_patch.ensure_path_with_pmethods(path) paver_patch.ensure_path_with_smethods(path) # ---------------------------------------------------------------------------- # PROJECT CONFIGURATION (for sdist/setup mostly): # ---------------------------------------------------------------------------- NAME = "parse_type" # ---------------------------------------------------------------------------- # TASK CONFIGURATION: # ---------------------------------------------------------------------------- options( # sphinx=Bunch( # # docroot=".", # sourcedir= "docs", # destdir = "../build/docs" # # XXX-behave: builddir="../build/docs" # ), minilib=Bunch( extra_files=[ 'doctools', 'virtual' ] ), pychecker = Bunch(default_args=NAME), pylint = Bunch(default_args=NAME), clean = Bunch( dirs = [ ".cache", ".tox", #< tox build subtree. "build", "dist", #< python setup temporary build dir. "tmp", ], files = [ ".coverage", "paver-minilib.zip", ], walkdirs_patterns = [ "__pycache__", #< Python compiled objects cache. "*.egg-info", ], walkfiles_patterns = [ "*.pyc", "*.pyo", "*$py.class", "*.bak", "*.log", "*.tmp", ".coverage.*", "pylint_*.txt", "pychecker_*.txt", ".DS_Store", "*.~*~", #< MACOSX ], ), pip = Bunch( requirements_files=[ "requirements/all.txt", ], # download_dir="downloads", download_dir= path("$HOME/.pip/downloads").expandvars(), ), ) # ---------------------------------------------------------------------------- # TASKS: # ---------------------------------------------------------------------------- @task @consume_args def default(args): """Default task, called when no task is provided (default: init).""" def help_function(): pass # paver.tasks.help(args, help_function) call_task("help", args=args) # @task # @consume_args # def docs(args): # """Generate the documentation: html, pdf, ... (default: html)""" # builders = args # if not builders: # builders = [ "html" ] # # call_task("prepare_docs") # for builder in builders: # sphinx_build(builder) # # @task # def linkcheck(): # """Check hyperlinks in documentation.""" # sphinx_build("linkcheck") # # ---------------------------------------------------------------------------- # TASKS: # ---------------------------------------------------------------------------- @task @consume_args def test(args): """Execute all tests""" py_test(args) # ---------------------------------------------------------------------------- # TASK: test coverage # ---------------------------------------------------------------------------- @task # @needs("coverage_collect") def coverage_report(): """Generate coverage report from collected coverage data.""" sh("coverage combine") sh("coverage report") sh("coverage html") info("WRITTEN TO: build/coverage.html/") # -- DISABLED: sh("coverage xml") @task @consume_args def coverage(args): """Execute all tests to collect code-coverage data, generate report.""" py_test(args, coverage_module=NAME) # ---------------------------------------------------------------------------- # TASK: clean # ---------------------------------------------------------------------------- @task def clean(options): """Cleanup the project workspace.""" for dir_ in options.dirs: path(dir_).rmtree_s() for pattern in options.walkdirs_patterns: dirs = path(".").walkdirs(pattern, errors="ignore") for dir_ in dirs: dir_.rmtree() for file_ in options.files: path(file_).remove_s() for pattern in options.walkfiles_patterns: files = path(".").walkfiles(pattern) for file_ in files: file_.remove() # ---------------------------------------------------------------------------- # UTILS: # ---------------------------------------------------------------------------- def python(cmdline, cwd="."): """Execute a python script by using the current python interpreter.""" return sh("%s %s" % (sys.executable, cmdline), cwd=cwd) def py_test(args=None, coverage_module=None, opts=""): """Execute all tests""" if not args: args = "" if coverage_module: os.environ["COVERAGE_HOME"] = os.path.abspath(os.getcwd()) opts += " --cov=%s" % coverage_module sh("py.test {opts} {args}".format(opts=opts, args=args)) def sphinx_build(builder="html", cmdopts=""): if builder.startswith("-"): cmdopts += " %s" % builder builder = "" sourcedir = options.sphinx.sourcedir destdir = options.sphinx.destdir command = "sphinx-build {opts} -b {builder} . {destdir}/{builder}".format( builder=builder, destdir=destdir, opts=cmdopts) sh(command, cwd=sourcedir) parse_type-0.3.4/pytest.ini000066400000000000000000000013671223602453100157270ustar00rootroot00000000000000# ============================================================================ # PYTEST CONFIGURATION FILE: pytest.ini # ============================================================================ # SEE ALSO: # * http://pytest.org/ # * http://pytest.org/customize.html # * http://pytest.org/example/pythoncollection.html#change-naming-conventions # ============================================================================ # MORE OPTIONS: # addopts = # python_classes=*Test # python_functions=test # ============================================================================ [pytest] minversion = 2.3 norecursedirs= .git .tox build dist tmp* _* python_files = test_*.py # usefixtures = tmpdir # markers = # xxx: mark an experimental test. parse_type-0.3.4/requirements/000077500000000000000000000000001223602453100164125ustar00rootroot00000000000000parse_type-0.3.4/requirements/all.txt000066400000000000000000000006421223602453100177250ustar00rootroot00000000000000# ============================================================================ # BEHAVE: PYTHON PACKAGE REQUIREMENTS: All requirements # ============================================================================ # DESCRIPTION: # pip install -r # # SEE ALSO: # * http://www.pip-installer.org/ # ============================================================================ -r basic.txt -r develop.txt parse_type-0.3.4/requirements/basic.txt000066400000000000000000000006531223602453100202400ustar00rootroot00000000000000# ============================================================================ # PYTHON PACKAGE REQUIREMENTS: Normal usage/installation (minimal) # ============================================================================ # DESCRIPTION: # pip install -r # # SEE ALSO: # * http://www.pip-installer.org/ # ============================================================================ parse >= 1.6.3 enum34 six parse_type-0.3.4/requirements/develop.txt000066400000000000000000000007041223602453100206120ustar00rootroot00000000000000# ============================================================================ # PYTHON PACKAGE REQUIREMENTS FOR: parse_type -- For development only # ============================================================================ # -- TESTING: Unit tests and behave self-tests. argparse pytest pytest-cov pytest-runner unittest2 # -- DEVELOPMENT SUPPORT: # PREPARED: paver >= 1.2 tox >= 1.6.1 coverage >= 3.7 # -- PROJECT ADMIN SUPPORT: bumpversion parse_type-0.3.4/requirements/docs.txt000066400000000000000000000004131223602453100201010ustar00rootroot00000000000000# ============================================================================ # PYTHON PACKAGE REQUIREMENTS: For documentation generation # ============================================================================ # sphinxcontrib-cheeseshop >= 0.2 sphinx >= 1.1 parse_type-0.3.4/setup.cfg000066400000000000000000000006231223602453100155110ustar00rootroot00000000000000# -- CONVENIENCE: Use pytest-runner (ptr) as test runner. [aliases] docs = build_sphinx test = ptr [build_sphinx] source-dir = docs/ build-dir = build/docs builder = html all_files = true [easy_install] # set the default location to install packages # install_dir = eggs # find_links = https://github.com/jenisys/parse_type [nosetests] where = tests [upload_docs] upload-dir = build/docs/html parse_type-0.3.4/setup.py000066400000000000000000000071141223602453100154040ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- """ wget https://bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.py python ez_setup.py SEE ALSO: * http://pypi.python.org/pypi/parse_type * https://github.com/jenisys/parse_type RELATED: * http://pypi.python.org/pypi/setuptools * https://bitbucket.org/pypa/setuptools/ * https://pythonhosted.org/setuptools/ * https://pythonhosted.org/setuptools/setuptools.html """ import sys import os.path sys.path.insert(0, os.curdir) # -- BOOTSTRAP: setuptools USE_BOOTSTRAP = os.environ.get("PYSETUP_BOOTSTRAP", "no") == "yes" if USE_BOOTSTRAP: from ez_setup import use_setuptools use_setuptools() # -- USE: setuptools from setuptools import setup, find_packages # ----------------------------------------------------------------------------- # PREPARE SETUP: # ----------------------------------------------------------------------------- HERE = os.path.dirname(__file__) python_version = float('%s.%s' % sys.version_info[:2]) requirements = ["parse>= 1.6", "six"] # requirements = ["parse", "six"] if python_version < 3.4: # -- NEED: Python3.4 enum types or enum34 backport requirements.append("enum34") README = os.path.join(HERE, "README.rst") long_description = ''.join(open(README).readlines()[4:]) extra = dict( # -- REQUIREMENTS: # setup_requires = ["setuptools>=1.0"], install_requires = requirements, tests_require = [], extras_require = { 'docs': ["sphinx>=1.1"], 'develop': [ "coverage", "pytest", "pytest-cov", "pytest-runner", "tox", ], }, test_suite = "tests", test_loader = "setuptools.command.test:ScanningLoader", zip_safe = True, ) if python_version < 2.7: extra["tests_require"].append("unittest2") if python_version >= 3.0: extra["use_2to3"] = True # -- NICE-TO-HAVE: # # FILE: setup.cfg -- Use pytest-runner (ptr) as test runner. # [aliases] # test = ptr USE_PYTEST_RUNNER = os.environ.get("PYSETUP_TEST", "pytest") == "pytest" if USE_PYTEST_RUNNER: extra["tests_require"].extend(["pytest", "pytest-runner"]) # ----------------------------------------------------------------------------- # SETUP: # ----------------------------------------------------------------------------- setup( name = "parse_type", version = "0.3.4", author = "Jens Engel", author_email = "jens_engel@nowhere.xxx", 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 = "BSD", # provides = ["parse_type"], # requires = requirements, packages = find_packages(exclude=["tests", "tests.*"]), include_package_data = True, classifiers = [ "Development Status :: 4 - Beta", "Environment :: Console", "Environment :: Web Environment", "Intended Audience :: Developers", "Operating System :: OS Independent", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Code Generators", "Topic :: Software Development :: Libraries :: Python Modules", "License :: OSI Approved :: BSD License", ], platforms = [ 'any' ], **extra ) parse_type-0.3.4/tests/000077500000000000000000000000001223602453100150315ustar00rootroot00000000000000parse_type-0.3.4/tests/__init__.py000066400000000000000000000000001223602453100171300ustar00rootroot00000000000000parse_type-0.3.4/tests/parse_type_test.py000077500000000000000000000121311223602453100206160ustar00rootroot00000000000000# -*- coding: utf-8 -*- 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.3.4/tests/test_builder.py000077500000000000000000000557271223602453100201130ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- """ Test suite for parse_type.py REQUIRES: parse >= 1.5.3.1 ('pattern' attribute support) """ 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 import parse import re import unittest # ----------------------------------------------------------------------------- # 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): from parse_type.parse import Parser as Parser2 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 = Parser2(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.3.4/tests/test_cardinality.py000077500000000000000000000565441223602453100207660ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- """ Test suite to test the :mod:`parse_type.cardinality` module. """ from .parse_type_test import ParseTypeTestCase, parse_number from parse_type import Cardinality, TypeBuilder, build_type_dict from parse_type.parse import Parser as ParserExt 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: ParserExt := 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 = ParserExt(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: ParserExt := 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 = ParserExt(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: ParserExt := 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 = ParserExt(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.3.4/tests/test_cardinality_field.py000077500000000000000000000376361223602453100221320ustar00rootroot00000000000000#!/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.3.4/tests/test_cardinality_field0.py000077500000000000000000000156221223602453100222010ustar00rootroot00000000000000#!/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.3.4/tests/test_cfparse.py000066400000000000000000000204611223602453100200700ustar00rootroot00000000000000# -*- coding: utf-8 -*- #!/usr/bin/env python # -*- coding: utf-8 -*- """ Test suite to test the :mod:`parse_type.cfparse` module. """ 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) 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.3.4/tests/test_parse_decorator.py000077500000000000000000000126371223602453100216320ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- """ Integrated into :mod:`parse` module. """ from __future__ import absolute_import from .parse_type_test import ParseTypeTestCase from parse_type import build_type_dict import parse import unittest # ----------------------------------------------------------------------------- # TEST CASE: TestParseTypeWithPatternDecorator # ----------------------------------------------------------------------------- class TestParseTypeWithPatternDecorator(ParseTypeTestCase): """ 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: 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: 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.3.4/tests/test_parse_type_parse.py000066400000000000000000000723621223602453100220210ustar00rootroot00000000000000# -*- coding: utf-8 -*- # BASED-ON: https://github.com/r1chardj0n3s/parse/ # VERSION: parse 1.6.3 # Same as original "test_parse.py" test except that parse_type copy is used. # -- 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. ''' import unittest from datetime import datetime, time # XXX-ADAPT: # ORIG: import parse from parse_type import parse # XXX-ADAPT-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('{{ }}', '\{ \}') def test_fixed(self): # pull a simple string out of another string self._test_expression('{}', '(.+?)') self._test_expression('{} {}', '(.+?) (.+?)') def test_named(self): # pull a named string out of another string self._test_expression('{name}', '(?P.+?)') self._test_expression('{name} {other}', '(?P.+?) (?P.+?)') def test_named_typed(self): # pull a named string out of another string self._test_expression('{name:w}', '(?P\w+)') self._test_expression('{name:w} {other:w}', '(?P\w+) (?P\w+)') def test_beaker(self): # skip some trailing whitespace self._test_expression('{:<}', '(.+?) *') def test_left_fill(self): # skip some trailing periods self._test_expression('{:.<}', '(.+?)\.*') def test_bird(self): # skip some trailing whitespace self._test_expression('{:>}', ' *(.+?)') def test_center(self): # skip some surrounding whitespace self._test_expression('{:^}', ' *(.+?) *') 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)) 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_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_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) 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_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_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_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_fail_value(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', .5) y('a {:%} b', 'a 50.1% b', .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) n('a {:f} b', 'a 12 b', None) y('a {:e} b', 'a 1.0e10 b', 1.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 {:=d} b', 'a 000012 b', 12) y('a {:x=5d} b', 'a xxx12 b', 12) y('a {:x=5d} b', 'a -xxx12 b', -12) 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) # 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): p = parse.compile('{:ti}' * 15) self.assertRaises(parse.TooManyFields, p.parse, '') 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) 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") class TestBugs(unittest.TestCase): 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, 2, 0, 45)) # ----------------------------------------------------------------------------- # 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 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") 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.3.4/tests/test_parse_util.py000066400000000000000000000412141223602453100206130ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- """ Test suite to test the :mod:`parse_type.parse_util` module. """ 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)), ] 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.3.4/tox.ini000066400000000000000000000076641223602453100152170ustar00rootroot00000000000000# ============================================================================ # TOX CONFIGURATION: parse_type # ============================================================================ # DESCRIPTION: # # Use tox to run tasks (tests, ...) in a clean virtual environment. # Tox is configured by default for offline usage. # Initialize local workspace once (download packages, create PyPI index): # # tox -e init1 # tox -e init2 (alternative) # # Afterwards you can run tox in offline mode, like: # # tox -e py27 # # NOTE: # You can either use "local1" or "local2" as local "tox.indexserver.default": # # * $HOME/.pip/downloads/ (local1, default) # * downloads/ (local2, alternative) # # SEE ALSO: # * http://tox.testrun.org/latest/config.html # ============================================================================ # NOTE: # virtualenv >= 1.10 dropped support for python2.5 # Use virtualenv==1.9.1 if you need python2.5 support # ============================================================================ # -- ONLINE USAGE: # PIP_INDEX_URL = http://pypi.python.org/simple [tox] minversion = 1.6.1 envlist = py26, py27, py32, py33, pypy, doctest sitepackages = False indexserver = default = file://{toxinidir}/downloads/simple local1 = file://{toxinidir}/downloads/simple local2 = file://{homedir}/.pip/downloads/simple pypi = http://pypi.python.org/simple # ----------------------------------------------------------------------------- # TEST ENVIRONMENTS: Bootstrap # ----------------------------------------------------------------------------- [testenv:init1] install_command = pip install --use-mirrors -i http://pypi.python.org/simple --find-links downloads --no-index {packages} changedir = {toxinidir} skipsdist = True commands= {toxinidir}/bin/toxcmd.py mkdir {toxinidir}/downloads pip install --use-mirrors --download={toxinidir}/downloads -r requirements/all.txt {toxinidir}/bin/make_localpi.py {toxinidir}/downloads deps= # setenv = # PIP_INDEX_URL = http://pypi.python.org/simple # PIP_DOWNLOAD_DIR = {toxinidir}/downloads # PIP_FIND_FILES = file://{toxinidir}/downloads [testenv:init2] install_command = pip install --use-mirrors -i http://pypi.python.org/simple --find-links downloads --no-index {packages} changedir = {toxinidir} skipsdist = True commands= {toxinidir}/bin/toxcmd.py mkdir {homedir}/.pip/downloads pip install --use-mirrors --download={homedir}/.pip/downloads -r requirements/all.txt {toxinidir}/bin/make_localpi.py {homedir}/.pip/downloads deps= # ----------------------------------------------------------------------------- # TEST ENVIRONMENTS: # ----------------------------------------------------------------------------- # install_command = pip install -U --pre {opts} {packages} [testenv] install_command = pip install -U {opts} {packages} changedir = {toxinidir} commands = py.test {posargs:tests} deps = pytest setenv = TOXRUN = yes PYSETUP_BOOTSTRAP = no PIP_FIND_FILES = {toxinidir}/downloads [testenv:py26] deps = {[testenv]deps} unittest2 [testenv:doctest] commands = py.test --doctest-modules -v parse_type # ----------------------------------------------------------------------------- # MORE TEST ENVIRONMENTS: # ----------------------------------------------------------------------------- [testenv:coverage] commands = py.test --cov=parse_type {posargs:tests} deps = {[testenv]deps} pytest-cov coverage [testenv:install] changedir = {envdir} commands = python ../../setup.py install -q {toxinidir}/bin/toxcmd.py copytree ../../tests . py.test {posargs:tests} deps = pytest # -- ENSURE: README.rst is well-formed. # python setup.py --long-description | rst2html.py >output.html #[testenv:check_README] #changedir = {toxinidir} #commands= # python setup.py --long-description > output.tmp # rst2html.py output.tmp output.html #deps = # docutils