pax_global_header 0000666 0000000 0000000 00000000064 12236024531 0014510 g ustar 00root root 0000000 0000000 52 comment=bd940f74275e37680284fe2fb044d12953b68ffb
parse_type-0.3.4/ 0000775 0000000 0000000 00000000000 12236024531 0013667 5 ustar 00root root 0000000 0000000 parse_type-0.3.4/.bumpversion.cfg 0000664 0000000 0000000 00000000171 12236024531 0016776 0 ustar 00root root 0000000 0000000 [bumpversion]
current_version = 0.3.4
files = setup.py parse_type/__init__.py .bumpversion.cfg
commit = True
tag = True
parse_type-0.3.4/.coveragerc 0000664 0000000 0000000 00000001604 12236024531 0016011 0 ustar 00root root 0000000 0000000 # =========================================================================
# 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/.editorconfig 0000664 0000000 0000000 00000001064 12236024531 0016345 0 ustar 00root root 0000000 0000000 # =============================================================================
# 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/.gitignore 0000664 0000000 0000000 00000000536 12236024531 0015663 0 ustar 00root root 0000000 0000000 *.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.yml 0000664 0000000 0000000 00000000340 12236024531 0015775 0 ustar 00root root 0000000 0000000 language: 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/LICENSE 0000664 0000000 0000000 00000002713 12236024531 0014677 0 ustar 00root root 0000000 0000000 Copyright (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.in 0000664 0000000 0000000 00000000506 12236024531 0015426 0 ustar 00root root 0000000 0000000 include 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.rst 0000664 0000000 0000000 00000022343 12236024531 0015362 0 ustar 00root root 0000000 0000000 ===============================================================================
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/ 0000775 0000000 0000000 00000000000 12236024531 0016023 5 ustar 00root root 0000000 0000000 parse_type-0.3.4/_paver_ext/__init__.py 0000664 0000000 0000000 00000000027 12236024531 0020133 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*- parse_type-0.3.4/_paver_ext/paver_consume_args.py 0000664 0000000 0000000 00000002364 12236024531 0022264 0 ustar 00root root 0000000 0000000 # -*- 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.py 0000664 0000000 0000000 00000006470 12236024531 0020700 0 ustar 00root root 0000000 0000000 # -*- 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.py 0000664 0000000 0000000 00000002163 12236024531 0021250 0 ustar 00root root 0000000 0000000 # -*- 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.py 0000664 0000000 0000000 00000006567 12236024531 0021072 0 ustar 00root root 0000000 0000000 # -*- 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.py 0000664 0000000 0000000 00000007150 12236024531 0021252 0 ustar 00root root 0000000 0000000 # ============================================================================
# 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.py 0000664 0000000 0000000 00000005220 12236024531 0021401 0 ustar 00root root 0000000 0000000 # -*- 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.py 0000664 0000000 0000000 00000003672 12236024531 0022531 0 ustar 00root root 0000000 0000000 # ============================================================================
# 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/ 0000775 0000000 0000000 00000000000 12236024531 0014437 5 ustar 00root root 0000000 0000000 parse_type-0.3.4/bin/make_localpi.py 0000775 0000000 0000000 00000017015 12236024531 0017440 0 ustar 00root root 0000000 0000000 #!/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.sh 0000775 0000000 0000000 00000001256 12236024531 0020545 0 ustar 00root root 0000000 0000000 #!/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.py 0000775 0000000 0000000 00000021072 12236024531 0016314 0 ustar 00root root 0000000 0000000 #!/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.py 0000664 0000000 0000000 00000027075 12236024531 0016112 0 ustar 00root root 0000000 0000000 #!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/ 0000775 0000000 0000000 00000000000 12236024531 0016042 5 ustar 00root root 0000000 0000000 parse_type-0.3.4/parse_type/__init__.py 0000664 0000000 0000000 00000002615 12236024531 0020157 0 ustar 00root root 0000000 0000000 # -*- 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.py 0000664 0000000 0000000 00000027071 12236024531 0020051 0 ustar 00root root 0000000 0000000 # -*- 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.py 0000664 0000000 0000000 00000017720 12236024531 0020726 0 ustar 00root root 0000000 0000000 # -*- 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.py 0000664 0000000 0000000 00000015303 12236024531 0022064 0 ustar 00root root 0000000 0000000 # -*- 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.py 0000664 0000000 0000000 00000006552 12236024531 0020047 0 ustar 00root root 0000000 0000000 # -*- 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.py 0000664 0000000 0000000 00000113637 12236024531 0017541 0 ustar 00root root 0000000 0000000 # -*- 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.py 0000664 0000000 0000000 00000012156 12236024531 0020570 0 ustar 00root root 0000000 0000000 # -*- 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.py 0000664 0000000 0000000 00000014136 12236024531 0016065 0 ustar 00root root 0000000 0000000 # ============================================================================
# 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.ini 0000664 0000000 0000000 00000001367 12236024531 0015727 0 ustar 00root root 0000000 0000000 # ============================================================================
# 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/ 0000775 0000000 0000000 00000000000 12236024531 0016412 5 ustar 00root root 0000000 0000000 parse_type-0.3.4/requirements/all.txt 0000664 0000000 0000000 00000000642 12236024531 0017725 0 ustar 00root root 0000000 0000000 # ============================================================================
# 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.txt 0000664 0000000 0000000 00000000653 12236024531 0020240 0 ustar 00root root 0000000 0000000 # ============================================================================
# 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.txt 0000664 0000000 0000000 00000000704 12236024531 0020612 0 ustar 00root root 0000000 0000000 # ============================================================================
# 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.txt 0000664 0000000 0000000 00000000413 12236024531 0020101 0 ustar 00root root 0000000 0000000 # ============================================================================
# PYTHON PACKAGE REQUIREMENTS: For documentation generation
# ============================================================================
# sphinxcontrib-cheeseshop >= 0.2
sphinx >= 1.1
parse_type-0.3.4/setup.cfg 0000664 0000000 0000000 00000000623 12236024531 0015511 0 ustar 00root root 0000000 0000000 # -- 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.py 0000664 0000000 0000000 00000007114 12236024531 0015404 0 ustar 00root root 0000000 0000000 #!/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/ 0000775 0000000 0000000 00000000000 12236024531 0015031 5 ustar 00root root 0000000 0000000 parse_type-0.3.4/tests/__init__.py 0000664 0000000 0000000 00000000000 12236024531 0017130 0 ustar 00root root 0000000 0000000 parse_type-0.3.4/tests/parse_type_test.py 0000775 0000000 0000000 00000012131 12236024531 0020616 0 ustar 00root root 0000000 0000000 # -*- 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.py 0000775 0000000 0000000 00000055727 12236024531 0020113 0 ustar 00root root 0000000 0000000 #!/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.py 0000775 0000000 0000000 00000056544 12236024531 0020766 0 ustar 00root root 0000000 0000000 #!/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.py 0000775 0000000 0000000 00000037636 12236024531 0022132 0 ustar 00root root 0000000 0000000 #!/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.py 0000775 0000000 0000000 00000015622 12236024531 0022201 0 ustar 00root root 0000000 0000000 #!/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.py 0000664 0000000 0000000 00000020461 12236024531 0020070 0 ustar 00root root 0000000 0000000 # -*- 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.py 0000775 0000000 0000000 00000012637 12236024531 0021632 0 ustar 00root root 0000000 0000000 #!/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.py 0000664 0000000 0000000 00000072362 12236024531 0022021 0 ustar 00root root 0000000 0000000 # -*- 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.py 0000664 0000000 0000000 00000041214 12236024531 0020613 0 ustar 00root root 0000000 0000000 #!/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.ini 0000664 0000000 0000000 00000007664 12236024531 0015217 0 ustar 00root root 0000000 0000000 # ============================================================================
# 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