pax_global_header 0000666 0000000 0000000 00000000064 13726616614 0014526 g ustar 00root root 0000000 0000000 52 comment=ce0656b3ff8026ba24e0ca205916814048a8c920
parse_type-0.5.6/ 0000775 0000000 0000000 00000000000 13726616614 0013711 5 ustar 00root root 0000000 0000000 parse_type-0.5.6/.bumpversion.cfg 0000664 0000000 0000000 00000000231 13726616614 0017015 0 ustar 00root root 0000000 0000000 [bumpversion]
current_version = 0.5.6
files = setup.py parse_type/__init__.py .bumpversion.cfg pytest.ini
commit = False
tag = False
allow_dirty = True
parse_type-0.5.6/.coveragerc 0000664 0000000 0000000 00000002174 13726616614 0016036 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 = True
omit = mock.py, ez_setup.py, distribute.py
[report]
ignore_errors = True
show_missing = True
# Regexes for lines to exclude from consideration
exclude_lines =
# Have to re-enable the standard pragma
pragma: no cover
# Don't complain about missing debug-only code:
def __repr__
if self\.debug
# Don't complain if tests don't hit defensive assertion code:
raise AssertionError
raise NotImplementedError
# Don't complain if non-runnable code isn't run:
if 0:
if False:
if __name__ == .__main__.:
[html]
directory = build/coverage.html
title = Coverage Report: parse_type
[xml]
output = build/coverage.xml
parse_type-0.5.6/.editorconfig 0000664 0000000 0000000 00000001064 13726616614 0016367 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.5.6/.gitignore 0000664 0000000 0000000 00000000500 13726616614 0015674 0 ustar 00root root 0000000 0000000 *.py[cod]
# Packages
MANIFEST
*.egg
*.egg-info
dist
build
downloads
__pycache__
# Installer logs
pip-log.txt
Pipfile
Pipfile.lock
# Unit test / coverage reports
.cache/
.eggs/
.pytest_cache/
.tox/
.venv*/
.coverage
# -- IDE-RELATED:
.idea/
.vscode/
.project
.pydevproject
# -- EXCLUDE GIT-SUBPROJECTS:
/lib/parse/
parse_type-0.5.6/.rosinstall 0000664 0000000 0000000 00000000301 13726616614 0016076 0 ustar 00root root 0000000 0000000 # GIT MULTI-REPO TOOL: wstool
# REQUIRES: wstool >= 0.1.17 (better: 0.1.18; not in pypi yet)
- git:
local-name: lib/parse
uri: https://github.com/r1chardj0n3s/parse
version: master
parse_type-0.5.6/.travis.yml 0000664 0000000 0000000 00000000547 13726616614 0016030 0 ustar 00root root 0000000 0000000 language: python
sudo: false
python:
- "3.7"
- "2.7"
- "3.8-dev"
- "pypy"
- "pypy3"
# -- TEST-BALLON: Check if Python 3.6 is actually Python 3.5.1 or newer
matrix:
allow_failures:
- python: "3.8-dev"
- python: "nightly"
install:
- pip install -U -r py.requirements/ci.travis.txt
- python setup.py -q install
script:
- pytest tests
parse_type-0.5.6/CHANGES.txt 0000664 0000000 0000000 00000000775 13726616614 0015533 0 ustar 00root root 0000000 0000000 Version History
===============================================================================
Version: 0.4.3 (2018-04-xx, unreleased)
-------------------------------------------------------------------------------
CHANGES:
* UPDATE: parse-1.8.3 (was: parse-1.8.2)
NOTE: ``parse`` module and ``parse_type.parse`` module are now identical.
BACKWARD INCOMPATIBLE CHANGES:
* RENAMED: type_converter.regex_group_count attribute (was: .group_count)
(pull-request review changes of the ``parse`` module).
parse_type-0.5.6/LICENSE 0000664 0000000 0000000 00000002720 13726616614 0014717 0 ustar 00root root 0000000 0000000 Copyright (c) 2013-2019, 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.5.6/MANIFEST.in 0000664 0000000 0000000 00000000704 13726616614 0015450 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
include *.yaml
include bin/invoke*
recursive-include bin *.sh *.py *.cmd
recursive-include py.requirements *.txt
recursive-include tasks *.py *.txt *.rst *.zip
recursive-include tests *.py
# -- DISABLED: recursive-include docs *.rst *.txt *.py
prune .tox
prune .venv*
parse_type-0.5.6/README.rst 0000664 0000000 0000000 00000022472 13726616614 0015407 0 ustar 00root root 0000000 0000000 ===============================================================================
parse_type
===============================================================================
.. image:: https://img.shields.io/travis/jenisys/parse_type/master.svg
:target: https://travis-ci.org/jenisys/parse_type
:alt: Travis CI Build Status
.. image:: https://img.shields.io/pypi/v/parse_type.svg
:target: https://pypi.python.org/pypi/parse_type
:alt: Latest Version
.. image:: https://img.shields.io/pypi/dm/parse_type.svg
:target: https://pypi.python.org/pypi/parse_type
:alt: Downloads
.. image:: https://img.shields.io/pypi/l/parse_type.svg
:target: https://pypi.python.org/pypi/parse_type/
:alt: License
`parse_type`_ extends the `parse`_ module (opposite of `string.format()`_)
with the following features:
* build type converters for common use cases (enum/mapping, choice)
* build a type converter with a cardinality constraint (0..1, 0..*, 1..*)
from the type converter with cardinality=1.
* compose a type converter from other type converters
* an extended parser that supports the CardinalityField naming schema
and creates missing type variants (0..1, 0..*, 1..*) from the
primary type converter
.. _parse_type: http://pypi.python.org/pypi/parse_type
.. _parse: http://pypi.python.org/pypi/parse
.. _`string.format()`: http://docs.python.org/library/string.html#format-string-syntax
Definitions
-------------------------------------------------------------------------------
*type converter*
A type converter function that converts a textual representation
of a value type into instance of this value type.
In addition, a type converter function is often annotated with attributes
that allows the `parse`_ module to use it in a generic way.
A type converter is also called a *parse_type* (a definition used here).
*cardinality field*
A naming convention for related types that differ in cardinality.
A cardinality field is a type name suffix in the format of a field.
It allows parse format expression, ala::
"{person:Person}" #< Cardinality: 1 (one; the normal case)
"{person:Person?}" #< Cardinality: 0..1 (zero or one = optional)
"{persons:Person*}" #< Cardinality: 0..* (zero or more = many0)
"{persons:Person+}" #< Cardinality: 1..* (one or more = many)
This naming convention mimics the relationship descriptions in UML diagrams.
Basic Example
-------------------------------------------------------------------------------
Define an own type converter for numbers (integers):
.. code-block:: python
# -- USE CASE:
def parse_number(text):
return int(text)
parse_number.pattern = r"\d+" # -- REGULAR EXPRESSION pattern for type.
This is equivalent to:
.. code-block:: python
import parse
@parse.with_pattern(r"\d+")
def parse_number(text):
return int(text)
assert hasattr(parse_number, "pattern")
assert parse_number.pattern == r"\d+"
.. code-block:: python
# -- USE CASE: Use the type converter with the parse module.
schema = "Hello {number:Number}"
parser = parse.Parser(schema, dict(Number=parse_number))
result = parser.parse("Hello 42")
assert result is not None, "REQUIRE: text matches the schema."
assert result["number"] == 42
result = parser.parse("Hello XXX")
assert result is None, "MISMATCH: text does not match the schema."
.. hint::
The described functionality above is standard functionality
of the `parse`_ module. It serves as introduction for the remaining cases.
Cardinality
-------------------------------------------------------------------------------
Create an type converter for "ManyNumbers" (List, separated with commas)
with cardinality "1..* = 1+" (many) from the type converter for a "Number".
.. code-block:: python
# -- USE CASE: Create new type converter with a cardinality constraint.
# CARDINALITY: many := one or more (1..*)
from parse import Parser
from parse_type import TypeBuilder
parse_numbers = TypeBuilder.with_many(parse_number, listsep=",")
schema = "List: {numbers:ManyNumbers}"
parser = Parser(schema, dict(ManyNumbers=parse_numbers))
result = parser.parse("List: 1, 2, 3")
assert result["numbers"] == [1, 2, 3]
Create an type converter for an "OptionalNumbers" with cardinality "0..1 = ?"
(optional) from the type converter for a "Number".
.. code-block:: python
# -- USE CASE: Create new type converter with cardinality constraint.
# CARDINALITY: optional := zero or one (0..1)
from parse import Parser
from parse_type import TypeBuilder
parse_optional_number = TypeBuilder.with_optional(parse_number)
schema = "Optional: {number:OptionalNumber}"
parser = Parser(schema, dict(OptionalNumber=parse_optional_number))
result = parser.parse("Optional: 42")
assert result["number"] == 42
result = parser.parse("Optional: ")
assert result["number"] == None
Enumeration (Name-to-Value Mapping)
-------------------------------------------------------------------------------
Create an type converter for an "Enumeration" from the description of
the mapping as dictionary.
.. code-block:: python
# -- USE CASE: Create a type converter for an enumeration.
from parse import Parser
from parse_type import TypeBuilder
parse_enum_yesno = TypeBuilder.make_enum({"yes": True, "no": False})
parser = Parser("Answer: {answer:YesNo}", dict(YesNo=parse_enum_yesno))
result = parser.parse("Answer: yes")
assert result["answer"] == True
Create an type converter for an "Enumeration" from the description of
the mapping as an enumeration class (`Python 3.4 enum`_ or the `enum34`_
backport; see also: `PEP-0435`_).
.. code-block:: python
# -- USE CASE: Create a type converter for enum34 enumeration class.
# NOTE: Use Python 3.4 or enum34 backport.
from parse import Parser
from parse_type import TypeBuilder
from enum import Enum
class Color(Enum):
red = 1
green = 2
blue = 3
parse_enum_color = TypeBuilder.make_enum(Color)
parser = Parser("Select: {color:Color}", dict(Color=parse_enum_color))
result = parser.parse("Select: red")
assert result["color"] is Color.red
.. _`Python 3.4 enum`: http://docs.python.org/3.4/library/enum.html#module-enum
.. _enum34: http://pypi.python.org/pypi/enum34
.. _PEP-0435: http://www.python.org/dev/peps/pep-0435
Choice (Name Enumeration)
-------------------------------------------------------------------------------
A Choice data type allows to select one of several strings.
Create an type converter for an "Choice" list, a list of unique names
(as string).
.. code-block:: python
from parse import Parser
from parse_type import TypeBuilder
parse_choice_yesno = TypeBuilder.make_choice(["yes", "no"])
schema = "Answer: {answer:ChoiceYesNo}"
parser = Parser(schema, dict(ChoiceYesNo=parse_choice_yesno))
result = parser.parse("Answer: yes")
assert result["answer"] == "yes"
Variant (Type Alternatives)
-------------------------------------------------------------------------------
Sometimes you need a type converter that can accept text for multiple
type converter alternatives. This is normally called a "variant" (or: union).
Create an type converter for an "Variant" type that accepts:
* Numbers (positive numbers, as integer)
* Color enum values (by name)
.. code-block:: python
from parse import Parser, with_pattern
from parse_type import TypeBuilder
from enum import Enum
class Color(Enum):
red = 1
green = 2
blue = 3
@with_pattern(r"\d+")
def parse_number(text):
return int(text)
# -- MAKE VARIANT: Alternatives of different type converters.
parse_color = TypeBuilder.make_enum(Color)
parse_variant = TypeBuilder.make_variant([parse_number, parse_color])
schema = "Variant: {variant:Number_or_Color}"
parser = Parser(schema, dict(Number_or_Color=parse_variant))
# -- TEST VARIANT: With number, color and mismatch.
result = parser.parse("Variant: 42")
assert result["variant"] == 42
result = parser.parse("Variant: blue")
assert result["variant"] is Color.blue
result = parser.parse("Variant: __MISMATCH__")
assert not result
Extended Parser with CardinalityField support
-------------------------------------------------------------------------------
The parser extends the ``parse.Parser`` and adds the following functionality:
* supports the CardinalityField naming scheme
* automatically creates missing type variants for types with
a CardinalityField by using the primary type converter for cardinality=1
* extends the provide type converter dictionary with new type variants.
Example:
.. code-block:: python
# -- USE CASE: Parser with CardinalityField support.
# NOTE: Automatically adds missing type variants with CardinalityField part.
# USE: parse_number() type converter from above.
from parse_type.cfparse import Parser
# -- PREPARE: parser, adds missing type variant for cardinality 1..* (many)
type_dict = dict(Number=parse_number)
schema = "List: {numbers:Number+}"
parser = Parser(schema, type_dict)
assert "Number+" in type_dict, "Created missing type variant based on: Number"
# -- USE: parser.
result = parser.parse("List: 1, 2, 3")
assert result["numbers"] == [1, 2, 3]
parse_type-0.5.6/bin/ 0000775 0000000 0000000 00000000000 13726616614 0014461 5 ustar 00root root 0000000 0000000 parse_type-0.5.6/bin/invoke 0000775 0000000 0000000 00000000253 13726616614 0015702 0 ustar 00root root 0000000 0000000 #!/bin/sh
#!/bin/bash
# RUN INVOKE: From bundled ZIP file.
HERE=$(dirname $0)
export INVOKE_TASKS_USE_VENDOR_BUNDLES="yes"
python ${HERE}/../tasks/_vendor/invoke.zip $*
parse_type-0.5.6/bin/invoke.cmd 0000664 0000000 0000000 00000000317 13726616614 0016442 0 ustar 00root root 0000000 0000000 @echo off
REM RUN INVOKE: From bundled ZIP file.
setlocal
set HERE=%~dp0
set INVOKE_TASKS_USE_VENDOR_BUNDLES="yes"
if not defined PYTHON set PYTHON=python
%PYTHON% %HERE%../tasks/_vendor/invoke.zip "%*"
parse_type-0.5.6/bin/make_localpi.py 0000775 0000000 0000000 00000017015 13726616614 0017462 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.5.6/bin/project_bootstrap.sh 0000775 0000000 0000000 00000001240 13726616614 0020560 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 --download=${PIP_DOWNLOAD_DIR} -r ${TOP}/requirements/all.txt
${HERE}/make_localpi.py ${PIP_DOWNLOAD_DIR}
parse_type-0.5.6/bin/toxcmd.py 0000775 0000000 0000000 00000021072 13726616614 0016336 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.5.6/invoke.yaml 0000664 0000000 0000000 00000001173 13726616614 0016072 0 ustar 00root root 0000000 0000000 # =====================================================
# INVOKE CONFIGURATION: parse_type
# =====================================================
# -- ON WINDOWS:
# run:
# echo: true
# pty: false
# shell: C:\Windows\System32\cmd.exe
# =====================================================
project:
name: parse_type
repo: "pypi"
# -- TODO: until upload problems are resolved.
repo_url: "https://upload.pypi.org/legacy/"
tasks:
auto_dash_names: false
run:
echo: true
# DISABLED: pty: true
cleanup_all:
extra_directories:
- build
- dist
- .hypothesis
- .pytest_cache
parse_type-0.5.6/parse_type/ 0000775 0000000 0000000 00000000000 13726616614 0016064 5 ustar 00root root 0000000 0000000 parse_type-0.5.6/parse_type/__init__.py 0000664 0000000 0000000 00000003050 13726616614 0020173 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
"""
This module extends the :mod:`parse` to build and derive additional
parse-types from other, existing types.
"""
from __future__ import absolute_import
from parse_type.cardinality import Cardinality
from parse_type.builder import TypeBuilder, build_type_dict
__all__ = ["Cardinality", "TypeBuilder", "build_type_dict"]
__version__ = "0.5.6"
# -----------------------------------------------------------------------------
# Copyright (c) 2012-2020 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.5.6/parse_type/builder.py 0000664 0000000 0000000 00000027577 13726616614 0020106 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# pylint: disable=missing-docstring
r"""
Provides support to compose user-defined parse types.
Cardinality
------------
It is often useful to constrain how often a data type occurs.
This is also called the cardinality of a data type (in a context).
The supported cardinality are:
* 0..1 zero_or_one, optional: T or None
* 0..N zero_or_more, list_of
* 1..N one_or_more, list_of (many)
.. doctest:: cardinality
>>> from parse_type import TypeBuilder
>>> from parse import Parser
>>> def parse_number(text):
... return int(text)
>>> parse_number.pattern = r"\d+"
>>> parse_many_numbers = TypeBuilder.with_many(parse_number)
>>> more_types = { "Numbers": parse_many_numbers }
>>> parser = Parser("List: {numbers:Numbers}", more_types)
>>> parser.parse("List: 1, 2, 3")
Enumeration Type (Name-to-Value Mappings)
-----------------------------------------
An Enumeration data type allows to select one of several enum values by using
its name. The converter function returns the selected enum value.
.. doctest:: make_enum
>>> parse_enum_yesno = TypeBuilder.make_enum({"yes": True, "no": False})
>>> more_types = { "YesNo": parse_enum_yesno }
>>> parser = Parser("Answer: {answer:YesNo}", more_types)
>>> parser.parse("Answer: yes")
Choice (Name Enumerations)
-----------------------------
A Choice data type allows to select one of several strings.
.. doctest:: make_choice
>>> parse_choice_yesno = TypeBuilder.make_choice(["yes", "no"])
>>> more_types = { "ChoiceYesNo": parse_choice_yesno }
>>> parser = Parser("Answer: {answer:ChoiceYesNo}", more_types)
>>> parser.parse("Answer: yes")
"""
from __future__ import absolute_import
import inspect
import re
import enum
from parse_type.cardinality import pattern_group_count, \
Cardinality, TypeBuilder as CardinalityTypeBuilder
__all__ = ["TypeBuilder", "build_type_dict", "parse_anything"]
class TypeBuilder(CardinalityTypeBuilder):
"""
Provides a utility class to build type-converters (parse_types) for
the :mod:`parse` module.
"""
default_strict = True
default_re_opts = (re.IGNORECASE | re.DOTALL)
@classmethod
def make_list(cls, item_converter=None, listsep=','):
"""
Create a type converter for a list of items (many := 1..*).
The parser accepts anything and the converter needs to fail on errors.
:param item_converter: Type converter for an item.
:param listsep: List separator to use (as string).
:return: Type converter function object for the list.
"""
if not item_converter:
item_converter = parse_anything
return cls.with_cardinality(Cardinality.many, item_converter,
pattern=cls.anything_pattern,
listsep=listsep)
@staticmethod
def make_enum(enum_mappings):
"""
Creates a type converter for an enumeration or text-to-value mapping.
:param enum_mappings: Defines enumeration names and values.
:return: Type converter function object for the enum/mapping.
"""
if (inspect.isclass(enum_mappings) and
issubclass(enum_mappings, enum.Enum)):
enum_class = enum_mappings
enum_mappings = enum_class.__members__
def convert_enum(text):
if text not in convert_enum.mappings:
text = text.lower() # REQUIRED-BY: parse re.IGNORECASE
return convert_enum.mappings[text] #< text.lower() ???
convert_enum.pattern = r"|".join(enum_mappings.keys())
convert_enum.mappings = enum_mappings
return convert_enum
@staticmethod
def _normalize_choices(choices, transform):
assert transform is None or callable(transform)
if transform:
choices = [transform(value) for value in choices]
else:
choices = list(choices)
return choices
@classmethod
def make_choice(cls, choices, transform=None, strict=None):
"""
Creates a type-converter function to select one from a list of strings.
The type-converter function returns the selected choice_text.
The :param:`transform()` function is applied in the type converter.
It can be used to enforce the case (because parser uses re.IGNORECASE).
:param choices: List of strings as choice.
:param transform: Optional, initial transform function for parsed text.
:return: Type converter function object for this choices.
"""
# -- NOTE: Parser uses re.IGNORECASE flag
# => transform may enforce case.
choices = cls._normalize_choices(choices, transform)
if strict is None:
strict = cls.default_strict
def convert_choice(text):
if transform:
text = transform(text)
if strict and text not in convert_choice.choices:
values = ", ".join(convert_choice.choices)
raise ValueError("%s not in: %s" % (text, values))
return text
convert_choice.pattern = r"|".join(choices)
convert_choice.choices = choices
return convert_choice
@classmethod
def make_choice2(cls, choices, transform=None, strict=None):
"""
Creates a type converter to select one item from a list of strings.
The type converter function returns a tuple (index, choice_text).
:param choices: List of strings as choice.
:param transform: Optional, initial transform function for parsed text.
:return: Type converter function object for this choices.
"""
choices = cls._normalize_choices(choices, transform)
if strict is None:
strict = cls.default_strict
def convert_choice2(text):
if transform:
text = transform(text)
if strict and text not in convert_choice2.choices:
values = ", ".join(convert_choice2.choices)
raise ValueError("%s not in: %s" % (text, values))
index = convert_choice2.choices.index(text)
return index, text
convert_choice2.pattern = r"|".join(choices)
convert_choice2.choices = choices
return convert_choice2
@classmethod
def make_variant(cls, converters, re_opts=None, compiled=False, strict=True):
"""
Creates a type converter for a number of type converter alternatives.
The first matching type converter is used.
REQUIRES: type_converter.pattern attribute
:param converters: List of type converters as alternatives.
:param re_opts: Regular expression options zu use (=default_re_opts).
:param compiled: Use compiled regexp matcher, if true (=False).
:param strict: Enable assertion checks.
:return: Type converter function object.
.. note::
Works only with named fields in :class:`parse.Parser`.
Parser needs group_index delta for unnamed/fixed fields.
This is not supported for user-defined types.
Otherwise, you need to use :class:`parse_type.parse.Parser`
(patched version of the :mod:`parse` module).
"""
# -- NOTE: Uses double-dispatch with regex pattern rematch because
# match is not passed through to primary type converter.
assert converters, "REQUIRE: Non-empty list."
if len(converters) == 1:
return converters[0]
if re_opts is None:
re_opts = cls.default_re_opts
pattern = r")|(".join([tc.pattern for tc in converters])
pattern = r"("+ pattern + ")"
group_count = len(converters)
for converter in converters:
group_count += pattern_group_count(converter.pattern)
if compiled:
convert_variant = cls.__create_convert_variant_compiled(converters,
re_opts,
strict)
else:
convert_variant = cls.__create_convert_variant(re_opts, strict)
convert_variant.pattern = pattern
convert_variant.converters = tuple(converters)
convert_variant.regex_group_count = group_count
return convert_variant
@staticmethod
def __create_convert_variant(re_opts, strict):
# -- USE: Regular expression pattern (compiled on use).
def convert_variant(text, m=None):
# pylint: disable=invalid-name, unused-argument, missing-docstring
for converter in convert_variant.converters:
if re.match(converter.pattern, text, re_opts):
return converter(text)
# -- pragma: no cover
assert not strict, "OOPS-VARIANT-MISMATCH: %s" % text
return None
return convert_variant
@staticmethod
def __create_convert_variant_compiled(converters, re_opts, strict):
# -- USE: Compiled regular expression matcher.
for converter in converters:
matcher = getattr(converter, "matcher", None)
if not matcher:
converter.matcher = re.compile(converter.pattern, re_opts)
def convert_variant(text, m=None):
# pylint: disable=invalid-name, unused-argument, missing-docstring
for converter in convert_variant.converters:
if converter.matcher.match(text):
return converter(text)
# -- pragma: no cover
assert not strict, "OOPS-VARIANT-MISMATCH: %s" % text
return None
return convert_variant
def build_type_dict(converters):
"""
Builds type dictionary for user-defined type converters,
used by :mod:`parse` module.
This requires that each type converter has a "name" attribute.
:param converters: List of type converters (parse_types)
:return: Type converter dictionary
"""
more_types = {}
for converter in converters:
assert callable(converter)
more_types[converter.name] = converter
return more_types
# -----------------------------------------------------------------------------
# COMMON TYPE CONVERTERS
# -----------------------------------------------------------------------------
def parse_anything(text, match=None, match_start=0):
"""
Provides a generic type converter that accepts anything and returns
the text (unchanged).
:param text: Text to convert (as string).
:return: Same text (as string).
"""
# pylint: disable=unused-argument
return text
parse_anything.pattern = TypeBuilder.anything_pattern
# -----------------------------------------------------------------------------
# Copyright (c) 2012-2020 by Jens Engel (https://github/jenisys/parse_type)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
parse_type-0.5.6/parse_type/cardinality.py 0000664 0000000 0000000 00000020560 13726616614 0020744 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 __future__ import absolute_import
from enum import Enum
# -----------------------------------------------------------------------------
# FUNCTIONS:
# -----------------------------------------------------------------------------
def pattern_group_count(pattern):
"""Count the pattern-groups within a regex-pattern (as text)."""
return pattern.replace(r"\(", "").count("(")
# -----------------------------------------------------------------------------
# CLASS: Cardinality (Enum Class)
# -----------------------------------------------------------------------------
class Cardinality(Enum):
"""Cardinality enumeration class to simplify building regular expression
patterns for a data type with the specified cardinality.
"""
# pylint: disable=bad-whitespace
__order__ = "one, zero_or_one, zero_or_more, one_or_more"
one = (None, 0)
zero_or_one = (r"(%s)?", 1) # SCHEMA: pattern
zero_or_more = (r"(%s)?(\s*%s\s*(%s))*", 3) # SCHEMA: pattern sep pattern
one_or_more = (r"(%s)(\s*%s\s*(%s))*", 3) # SCHEMA: pattern sep pattern
# -- ALIASES:
optional = zero_or_one
many0 = zero_or_more
many = one_or_more
def __init__(self, schema, group_count=0):
self.schema = schema
self.group_count = group_count #< Number of match groups.
def is_many(self):
"""Checks for a more general interpretation of "many".
:return: True, if Cardinality.zero_or_more or Cardinality.one_or_more.
"""
return ((self is Cardinality.zero_or_more) or
(self is Cardinality.one_or_more))
def make_pattern(self, pattern, listsep=','):
"""Make pattern for a data type with the specified cardinality.
.. code-block:: python
yes_no_pattern = r"yes|no"
many_yes_no = Cardinality.one_or_more.make_pattern(yes_no_pattern)
:param pattern: Regular expression for type (as string).
:param listsep: List separator for multiple items (as string, optional)
:return: Regular expression pattern for type with cardinality.
"""
if self is Cardinality.one:
return pattern
elif self is Cardinality.zero_or_one:
return self.schema % pattern
# -- OTHERWISE:
return self.schema % (pattern, listsep, pattern)
def compute_group_count(self, pattern):
"""Compute the number of regexp match groups when the pattern is provided
to the :func:`Cardinality.make_pattern()` method.
:param pattern: Item regexp pattern (as string).
:return: Number of regexp match groups in the cardinality pattern.
"""
group_count = self.group_count
pattern_repeated = 1
if self.is_many():
pattern_repeated = 2
return group_count + pattern_repeated * pattern_group_count(pattern)
# -----------------------------------------------------------------------------
# CLASS: TypeBuilder
# -----------------------------------------------------------------------------
class TypeBuilder(object):
"""Provides a utility class to build type-converters (parse_types) for parse.
It supports to build new type-converters for different cardinality
based on the type-converter for cardinality one.
"""
anything_pattern = r".+?"
default_pattern = anything_pattern
@classmethod
def with_cardinality(cls, cardinality, converter, pattern=None,
listsep=','):
"""Creates a type converter for the specified cardinality
by using the type converter for T.
:param cardinality: Cardinality to use (0..1, 0..*, 1..*).
:param converter: Type converter (function) for data type T.
:param pattern: Regexp pattern for an item (=converter.pattern).
:return: type-converter for optional (T or None).
"""
if cardinality is Cardinality.one:
return converter
# -- NORMAL-CASE
builder_func = getattr(cls, "with_%s" % cardinality.name)
if cardinality is Cardinality.zero_or_one:
return builder_func(converter, pattern)
# -- MANY CASE: 0..*, 1..*
return builder_func(converter, pattern, listsep=listsep)
@classmethod
def with_zero_or_one(cls, converter, pattern=None):
"""Creates a type converter for a T with 0..1 times
by using the type converter for one item of T.
:param converter: Type converter (function) for data type T.
:param pattern: Regexp pattern for an item (=converter.pattern).
:return: type-converter for optional (T or None).
"""
cardinality = Cardinality.zero_or_one
if not pattern:
pattern = getattr(converter, "pattern", cls.default_pattern)
optional_pattern = cardinality.make_pattern(pattern)
group_count = cardinality.compute_group_count(pattern)
def convert_optional(text, m=None):
# pylint: disable=invalid-name, unused-argument, missing-docstring
if text:
text = text.strip()
if not text:
return None
return converter(text)
convert_optional.pattern = optional_pattern
convert_optional.regex_group_count = group_count
return convert_optional
@classmethod
def with_zero_or_more(cls, converter, pattern=None, listsep=","):
"""Creates a type converter function for a list with 0..N items
by using the type converter for one item of T.
:param converter: Type converter (function) for data type T.
:param pattern: Regexp pattern for an item (=converter.pattern).
:param listsep: Optional list separator between items (default: ',')
:return: type-converter for list
"""
cardinality = Cardinality.zero_or_more
if not pattern:
pattern = getattr(converter, "pattern", cls.default_pattern)
many0_pattern = cardinality.make_pattern(pattern, listsep)
group_count = cardinality.compute_group_count(pattern)
def convert_list0(text, m=None):
# pylint: disable=invalid-name, unused-argument, missing-docstring
if text:
text = text.strip()
if not text:
return []
return [converter(part.strip()) for part in text.split(listsep)]
convert_list0.pattern = many0_pattern
# OLD convert_list0.group_count = group_count
convert_list0.regex_group_count = group_count
return convert_list0
@classmethod
def with_one_or_more(cls, converter, pattern=None, listsep=","):
"""Creates a type converter function for a list with 1..N items
by using the type converter for one item of T.
:param converter: Type converter (function) for data type T.
:param pattern: Regexp pattern for an item (=converter.pattern).
:param listsep: Optional list separator between items (default: ',')
:return: Type converter for list
"""
cardinality = Cardinality.one_or_more
if not pattern:
pattern = getattr(converter, "pattern", cls.default_pattern)
many_pattern = cardinality.make_pattern(pattern, listsep)
group_count = cardinality.compute_group_count(pattern)
def convert_list(text, m=None):
# pylint: disable=invalid-name, unused-argument, missing-docstring
return [converter(part.strip()) for part in text.split(listsep)]
convert_list.pattern = many_pattern
# OLD: convert_list.group_count = group_count
convert_list.regex_group_count = group_count
return convert_list
# -- ALIAS METHODS:
@classmethod
def with_optional(cls, converter, pattern=None):
"""Alias for :py:meth:`with_zero_or_one()` method."""
return cls.with_zero_or_one(converter, pattern)
@classmethod
def with_many(cls, converter, pattern=None, listsep=','):
"""Alias for :py:meth:`with_one_or_more()` method."""
return cls.with_one_or_more(converter, pattern, listsep)
@classmethod
def with_many0(cls, converter, pattern=None, listsep=','):
"""Alias for :py:meth:`with_zero_or_more()` method."""
return cls.with_zero_or_more(converter, pattern, listsep)
parse_type-0.5.6/parse_type/cardinality_field.py 0000664 0000000 0000000 00000015256 13726616614 0022115 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
import six
from parse_type.cardinality import Cardinality, TypeBuilder
class MissingTypeError(KeyError): # pylint: disable=missing-docstring
pass
# -----------------------------------------------------------------------------
# CLASS: Cardinality (Field Part)
# -----------------------------------------------------------------------------
class CardinalityField(object):
"""Cardinality field for parse format expression, ala:
"{person:Person?}" #< Cardinality: 0..1 = zero or one = optional
"{persons:Person*}" #< Cardinality: 0..* = zero or more = many0
"{persons:Person+}" #< Cardinality: 1..* = one or more = many
"""
# -- MAPPING SUPPORT:
pattern_chars = "?*+"
from_char_map = {
'?': Cardinality.zero_or_one,
'*': Cardinality.zero_or_more,
'+': Cardinality.one_or_more,
}
to_char_map = dict([(value, key) for key, value in from_char_map.items()])
@classmethod
def matches_type(cls, type_name):
"""Checks if a type name uses the CardinalityField naming scheme.
:param type_name: Type name to check (as string).
:return: True, if type name has CardinalityField name suffix.
"""
return type_name and type_name[-1] in CardinalityField.pattern_chars
@classmethod
def split_type(cls, type_name):
"""Split type of a type name with CardinalityField suffix into its parts.
:param type_name: Type name (as string).
:return: Tuple (type_basename, cardinality)
"""
if cls.matches_type(type_name):
basename = type_name[:-1]
cardinality = cls.from_char_map[type_name[-1]]
else:
# -- ASSUME: Cardinality.one
cardinality = Cardinality.one
basename = type_name
return (basename, cardinality)
@classmethod
def make_type(cls, basename, cardinality):
"""Build new type name according to CardinalityField naming scheme.
:param basename: Type basename of primary type (as string).
:param cardinality: Cardinality of the new type (as Cardinality item).
:return: Type name with CardinalityField suffix (if needed)
"""
if cardinality is Cardinality.one:
# -- POSTCONDITION: assert not cls.make_type(type_name)
return basename
# -- NORMAL CASE: type with CardinalityField suffix.
type_name = "%s%s" % (basename, cls.to_char_map[cardinality])
# -- POSTCONDITION: assert cls.make_type(type_name)
return type_name
# -----------------------------------------------------------------------------
# CLASS: CardinalityFieldTypeBuilder
# -----------------------------------------------------------------------------
class CardinalityFieldTypeBuilder(object):
"""Utility class to create type converters based on:
* the CardinalityField naming scheme and
* type converter for cardinality=1
"""
listsep = ','
@classmethod
def create_type_variant(cls, type_name, type_converter):
r"""Create type variants for types with a cardinality field.
The new type converters are based on the type converter with
cardinality=1.
.. code-block:: python
import parse
@parse.with_pattern(r'\d+')
def parse_number(text):
return int(text)
new_type = CardinalityFieldTypeBuilder.create_type_variant(
"Number+", parse_number)
new_type = CardinalityFieldTypeBuilder.create_type_variant(
"Number+", dict(Number=parse_number))
:param type_name: Type name with cardinality field suffix.
:param type_converter: Type converter or type dictionary.
:return: Type converter variant (function).
:raises: ValueError, if type_name does not end with CardinalityField
:raises: MissingTypeError, if type_converter is missing in type_dict
"""
assert isinstance(type_name, six.string_types)
if not CardinalityField.matches_type(type_name):
message = "type_name='%s' has no CardinalityField" % type_name
raise ValueError(message)
primary_name, cardinality = CardinalityField.split_type(type_name)
if isinstance(type_converter, dict):
type_dict = type_converter
type_converter = type_dict.get(primary_name, None)
if not type_converter:
raise MissingTypeError(primary_name)
assert callable(type_converter)
type_variant = TypeBuilder.with_cardinality(cardinality,
type_converter,
listsep=cls.listsep)
type_variant.name = type_name
return type_variant
@classmethod
def create_type_variants(cls, type_names, type_dict):
"""Create type variants for types with a cardinality field.
The new type converters are based on the type converter with
cardinality=1.
.. code-block:: python
# -- USE: parse_number() type converter function.
new_types = CardinalityFieldTypeBuilder.create_type_variants(
["Number?", "Number+"], dict(Number=parse_number))
:param type_names: List of type names with cardinality field suffix.
:param type_dict: Type dictionary with named type converters.
:return: Type dictionary with type converter variants.
"""
type_variant_dict = {}
for type_name in type_names:
type_variant = cls.create_type_variant(type_name, type_dict)
type_variant_dict[type_name] = type_variant
return type_variant_dict
# MAYBE: Check if really needed.
@classmethod
def create_missing_type_variants(cls, type_names, type_dict):
"""Create missing type variants for types with a cardinality field.
:param type_names: List of type names with cardinality field suffix.
:param type_dict: Type dictionary with named type converters.
:return: Type dictionary with missing type converter variants.
"""
missing_type_names = [name for name in type_names
if name not in type_dict]
return cls.create_type_variants(missing_type_names, type_dict)
parse_type-0.5.6/parse_type/cfparse.py 0000664 0000000 0000000 00000007275 13726616614 0020074 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
import logging
import parse
from .cardinality_field import CardinalityField, CardinalityFieldTypeBuilder
from .parse_util import FieldParser
log = logging.getLogger(__name__) # pylint: disable=invalid-name
class Parser(parse.Parser):
"""Provides an extended :class:`parse.Parser` with cardinality field support.
A cardinality field is a type suffix for parse format expression, ala:
"... {person:Person?} ..." -- OPTIONAL: Cardinality zero or one, 0..1
"... {persons:Person*} ..." -- MANY0: Cardinality zero or more, 0..
"... {persons:Person+} ..." -- MANY: Cardinality one or more, 1..
When the primary type converter for cardinality=1 is provided,
the type variants for the other cardinality cases can be derived from it.
This parser class automatically creates missing type variants for types
with a cardinality field and passes the extended type dictionary
to its base class.
"""
# -- TYPE-BUILDER: For missing types in Fields with CardinalityField part.
type_builder = CardinalityFieldTypeBuilder
def __init__(self, schema, extra_types=None, case_sensitive=False,
type_builder=None):
"""Creates a parser with CardinalityField part support.
:param schema: Parse schema (or format) for parser (as string).
:param extra_types: Type dictionary with type converters (or None).
:param case_sensitive: Indicates if case-sensitive regexp are used.
:param type_builder: Type builder to use for missing types.
"""
if extra_types is None:
extra_types = {}
missing = self.create_missing_types(schema, extra_types, type_builder)
if missing:
# pylint: disable=logging-not-lazy
log.debug("MISSING TYPES: %s" % ",".join(missing.keys()))
extra_types.update(missing)
# -- FINALLY: Delegate to base class.
super(Parser, self).__init__(schema, extra_types,
case_sensitive=case_sensitive)
@classmethod
def create_missing_types(cls, schema, type_dict, type_builder=None):
"""Creates missing types for fields with a CardinalityField part.
It is assumed that the primary type converter for cardinality=1
is registered in the type dictionary.
:param schema: Parse schema (or format) for parser (as string).
:param type_dict: Type dictionary with type converters.
:param type_builder: Type builder to use for missing types.
:return: Type dictionary with missing types. Empty, if none.
:raises: MissingTypeError,
if a primary type converter with cardinality=1 is missing.
"""
if not type_builder:
type_builder = cls.type_builder
missing = cls.extract_missing_special_type_names(schema, type_dict)
return type_builder.create_type_variants(missing, type_dict)
@staticmethod
def extract_missing_special_type_names(schema, type_dict):
# pylint: disable=invalid-name
"""Extract the type names for fields with CardinalityField part.
Selects only the missing type names that are not in the type dictionary.
:param schema: Parse schema to use (as string).
:param type_dict: Type dictionary with type converters.
:return: Generator with missing type names (as string).
"""
for name in FieldParser.extract_types(schema):
if CardinalityField.matches_type(name) and (name not in type_dict):
yield name
parse_type-0.5.6/parse_type/parse.py 0000664 0000000 0000000 00000145760 13726616614 0017565 0 ustar 00root root 0000000 0000000 # -*- coding: UTF-8 -*-
# BASED-ON: https://github.com/r1chardj0n3s/parse/parse.py
# VERSION: parse 1.18.0
# Same as original parse modules.
#
# pylint: disable=line-too-long, invalid-name, too-many-locals, too-many-arguments
# pylint: disable=redefined-builtin, too-few-public-methods, no-else-return
# pylint: disable=unused-variable, no-self-use, missing-docstring
# pylint: disable=unused-argument, unused-variable
# pylint: disable=too-many-branches, too-many-statements
# pylint: disable=all
#
# -- ORIGINAL-CODE STARTS-HERE ------------------------------------------------
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()``, ``findall()``,
and ``with_pattern()`` when ``import \*`` is used:
>>> from parse import *
From there it's a simple thing to parse a string:
.. code-block:: pycon
>>> parse("It's {}, I love it!", "It's spam, I love it!")
>>> _[0]
'spam'
Or to search a string for some pattern:
.. code-block:: pycon
>>> search('Age: {:d}\n', 'Name: Rufus\nAge: 42\nColor: red\n')
Or find all the occurrences of some pattern in a string:
.. code-block:: pycon
>>> ''.join(r[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:
.. code-block:: pycon
>>> 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)
The default behaviour is to match strings case insensitively. You may match with
case by specifying `case_sensitive=True`:
.. code-block:: pycon
>>> parse('SPAM', 'spam', case_sensitive=True) is None
True
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 imply dictionaries (see below for example).
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:
.. code-block:: pycon
>>> 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
>>> 'item' in r
True
Note that `in` only works if you have named fields. Dotted names and indexes
are possible though the application must make additional sense of the result:
.. code-block:: pycon
>>> 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
>>> r = parse("My quest is {quest[name]}", "My quest is to seek the holy grail!")
>>> print(r)
>>> print(r['quest'])
{'name': 'to seek the holy grail!'}
>>> print(r['quest']['name'])
to seek the holy grail!
If the text you're matching has braces in it you can match those by including
a double-brace ``{{`` or ``}}`` in your format string, just like format() does.
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][.precision][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. The "e" type handles Fortran formatted numbers (no
leading 0 before the decimal point).
===== =========================================== ========
Type Characters Matched Output
===== =========================================== ========
l Letters (ASCII) str
w Letters, numbers and underscore str
W Not letters, numbers 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
F Decimal numbers Decimal
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
ts Linux system log format date/time datetime
e.g. Nov 9 03:37:44
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:
.. code-block:: pycon
>>> 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:
.. code-block:: pycon
>>> 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.
Width and precision may be used to restrict the size of matched text
from the input. Width specifies a minimum size and precision specifies
a maximum. For example:
.. code-block:: pycon
>>> parse('{:.2}{:.2}', 'look') # specifying precision
>>> parse('{:4}{:4}', 'look at that') # specifying width
>>> parse('{:4}{:.4}', 'look at that') # specifying both
>>> parse('{:2d}{:2d}', '0440') # parsing two contiguous numbers
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 and Match Objects
------------------------
The result of a ``parse()`` and ``search()`` operation is either ``None`` (no match), a
``Result`` instance or a ``Match`` instance if ``evaluate_result`` is False.
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).
The ``Match`` instance has one method:
``evaluate_result()``
Generates and returns a ``Result`` instance for this ``Match`` object.
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:
.. code-block:: pycon
>>> 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):
.. code-block:: pycon
>>> 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:
.. code-block:: pycon
>>> 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:
.. code-block:: pycon
>>> 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()]
If the type converter ``pattern`` uses regex-grouping (with parenthesis),
you should indicate this by using the optional ``regex_group_count`` parameter
in the ``with_pattern()`` decorator:
.. code-block:: pycon
>>> @with_pattern(r'((\d+))', regex_group_count=2)
... def parse_number2(text):
... return int(text)
>>> parse('Answer: {:Number2} {:Number2}', 'Answer: 42 43', dict(Number2=parse_number2))
Otherwise, this may cause parsing problems with unnamed/fixed parameters.
Potential Gotchas
-----------------
``parse()`` will always match the shortest text necessary (from left to right)
to fulfil the parse pattern, so for example:
.. code-block:: pycon
>>> pattern = '{dir1}/{dir2}'
>>> data = 'root/parent/subdir'
>>> sorted(parse(pattern, data).named.items())
[('dir1', 'root'), ('dir2', 'parent/subdir')]
So, even though `{'dir1': 'root/parent', 'dir2': 'subdir'}` would also fit
the pattern, the actual match represents the shortest successful match for
``dir1``.
----
- 1.18.0 Correct bug in int parsing introduced in 1.16.0 (thanks @maxxk)
- 1.17.0 Make left- and center-aligned search consume up to next space
- 1.16.0 Make compiled parse objects pickleable (thanks @martinResearch)
- 1.15.0 Several fixes for parsing non-base 10 numbers (thanks @vladikcomper)
- 1.14.0 More broad acceptance of Fortran number format (thanks @purpleskyfall)
- 1.13.1 Project metadata correction.
- 1.13.0 Handle Fortran formatted numbers with no leading 0 before decimal
point (thanks @purpleskyfall).
Handle comparison of FixedTzOffset with other types of object.
- 1.12.1 Actually use the `case_sensitive` arg in compile (thanks @jacquev6)
- 1.12.0 Do not assume closing brace when an opening one is found (thanks @mattsep)
- 1.11.1 Revert having unicode char in docstring, it breaks Bamboo builds(?!)
- 1.11.0 Implement `__contains__` for Result instances.
- 1.10.0 Introduce a "letters" matcher, since "w" matches numbers
also.
- 1.9.1 Fix deprecation warnings around backslashes in regex strings
(thanks Mickael Schoentgen). Also fix some documentation formatting
issues.
- 1.9.0 We now honor precision and width specifiers when parsing numbers
and strings, allowing parsing of concatenated elements of fixed width
(thanks Julia Signell)
- 1.8.4 Add LICENSE file at request of packagers.
Correct handling of AM/PM to follow most common interpretation.
Correct parsing of hexadecimal that looks like a binary prefix.
Add ability to parse case sensitively.
Add parsing of numbers to Decimal with "F" (thanks John Vandenberg)
- 1.8.3 Add regex_group_count to with_pattern() decorator to support
user-defined types that contain brackets/parenthesis (thanks Jens Engel)
- 1.8.2 add documentation for including braces in format string
- 1.8.1 ensure bare hexadecimal digits are not matched
- 1.8.0 support manual control over result evaluation (thanks Timo Furrer)
- 1.7.0 parse dict fields (thanks Mark Visser) and adapted to allow
more than 100 re groups in Python 3.5+ (thanks David King)
- 1.6.6 parse Linux system log dates (thanks Alex Cowan)
- 1.6.5 handle precision in float format (thanks Levi Kilcher)
- 1.6.4 handle pipe "|" characters in parse string (thanks Martijn Pieters)
- 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-2020 Richard Jones
See the end of the source file for the license of use.
'''
from __future__ import absolute_import
__version__ = '1.18.0'
# yes, I now have two problems
import re
import sys
from datetime import datetime, time, tzinfo, timedelta
from decimal import Decimal
from functools import partial
import logging
__all__ = 'parse search findall with_pattern'.split()
log = logging.getLogger(__name__)
def with_pattern(pattern, regex_group_count=None):
r"""Attach a regular expression pattern matcher to a custom type converter
function.
This annotates the type converter with the :attr:`pattern` attribute.
EXAMPLE:
>>> import parse
>>> @parse.with_pattern(r"\d+")
... def parse_number(text):
... return int(text)
is equivalent to:
>>> def parse_number(text):
... return int(text)
>>> parse_number.pattern = r"\d+"
:param pattern: regular expression pattern (as text)
:param regex_group_count: Indicates how many regex-groups are in pattern.
:return: wrapped function
"""
def decorator(func):
func.pattern = pattern
func.regex_group_count = regex_group_count
return func
return decorator
class int_convert:
"""Convert a string to an integer.
The string may start with a sign.
It may be of a base other than 2, 8, 10 or 16.
If base isn't specified, it will be detected automatically based
on a string format. When string starts with a base indicator, 0#nnnn,
it overrides the default base of 10.
It may also have other non-numeric characters that we can ignore.
"""
CHARS = '0123456789abcdefghijklmnopqrstuvwxyz'
def __init__(self, base=None):
self.base = base
def __call__(self, string, match):
if string[0] == '-':
sign = -1
number_start = 1
elif string[0] == '+':
sign = 1
number_start = 1
else:
sign = 1
number_start = 0
base = self.base
# If base wasn't specified, detect it automatically
if base is None:
# Assume decimal number, unless different base is detected
base = 10
# For number formats starting with 0b, 0o, 0x, use corresponding base ...
if string[number_start] == '0' and len(string) - number_start > 2:
if string[number_start + 1] in 'bB':
base = 2
elif string[number_start + 1] in 'oO':
base = 8
elif string[number_start + 1] in 'xX':
base = 16
chars = int_convert.CHARS[: base]
string = re.sub('[^%s]' % chars, '', string.lower())
return sign * int(string, base)
class convert_first:
"""Convert the first element of a pair.
This equivalent to lambda s,m: converter(s). But unlike a lambda function, it can be pickled
"""
def __init__(self, converter):
self.converter = converter
def __call__(self, string, match):
return self.converter(string)
def percentage(string, match):
return float(string[:-1]) / 100.0
class FixedTzOffset(tzinfo):
"""Fixed offset in minutes east from UTC."""
ZERO = timedelta(0)
def __init__(self, offset, name):
self._offset = timedelta(minutes=offset)
self._name = name
def __repr__(self):
return '<%s %s %s>' % (self.__class__.__name__, self._name, self._offset)
def utcoffset(self, dt):
return self._offset
def tzname(self, dt):
return self._name
def dst(self, dt):
return self.ZERO
def __eq__(self, other):
if not isinstance(other, FixedTzOffset):
return False
return self._name == other._name and self._offset == other._offset
MONTHS_MAP = dict(
Jan=1,
January=1,
Feb=2,
February=2,
Mar=3,
March=3,
Apr=4,
April=4,
May=5,
Jun=6,
June=6,
Jul=7,
July=7,
Aug=8,
August=8,
Sep=9,
September=9,
Oct=10,
October=10,
Nov=11,
November=11,
Dec=12,
December=12,
)
DAYS_PAT = r'(Mon|Tue|Wed|Thu|Fri|Sat|Sun)'
MONTHS_PAT = r'(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)'
ALL_MONTHS_PAT = r'(%s)' % '|'.join(MONTHS_MAP)
TIME_PAT = r'(\d{1,2}:\d{1,2}(:\d{1,2}(\.\d+)?)?)'
AM_PAT = r'(\s+[AP]M)'
TZ_PAT = r'(\s+[-+]\d\d?:?\d\d)'
def date_convert(
string,
match,
ymd=None,
mdy=None,
dmy=None,
d_m_y=None,
hms=None,
am=None,
tz=None,
mm=None,
dd=None,
):
"""Convert the incoming string containing some date / time info into a
datetime instance.
"""
groups = match.groups()
time_only = False
if mm and dd:
y = datetime.today().year
m = groups[mm]
d = groups[dd]
elif ymd is not None:
y, m, d = re.split(r'[-/\s]', groups[ymd])
elif mdy is not None:
m, d, y = re.split(r'[-/\s]', groups[mdy])
elif dmy is not None:
d, m, y = re.split(r'[-/\s]', groups[dmy])
elif d_m_y is not None:
d, m, y = d_m_y
d = groups[d]
m = groups[m]
y = groups[y]
else:
time_only = True
H = M = S = u = 0
if hms is not None and groups[hms]:
t = groups[hms].split(':')
if len(t) == 2:
H, M = t
else:
H, M, S = t
if '.' in S:
S, u = S.split('.')
u = int(float('.' + u) * 1000000)
S = int(S)
H = int(H)
M = int(M)
if am is not None:
am = groups[am]
if am:
am = am.strip()
if am == 'AM' and H == 12:
# correction for "12" hour functioning as "0" hour: 12:15 AM = 00:15 by 24 hr clock
H -= 12
elif am == 'PM' and H == 12:
# no correction needed: 12PM is midday, 12:00 by 24 hour clock
pass
elif am == 'PM':
H += 12
if tz is not None:
tz = groups[tz]
if tz == 'Z':
tz = FixedTzOffset(0, 'UTC')
elif tz:
tz = tz.strip()
if tz.isupper():
# TODO use the awesome python TZ module?
pass
else:
sign = tz[0]
if ':' in tz:
tzh, tzm = tz[1:].split(':')
elif len(tz) == 4: # 'snnn'
tzh, tzm = tz[1], tz[2:4]
else:
tzh, tzm = tz[1:3], tz[3:5]
offset = int(tzm) + int(tzh) * 60
if sign == '-':
offset = -offset
tz = FixedTzOffset(offset, tz)
if time_only:
d = time(H, M, S, u, tzinfo=tz)
else:
y = int(y)
if m.isdigit():
m = int(m)
else:
m = MONTHS_MAP[m]
d = int(d)
d = datetime(y, m, d, H, M, S, u, tzinfo=tz)
return d
class TooManyFields(ValueError):
pass
class RepeatedNameError(ValueError):
pass
# note: {} are handled separately
# note: I don't use r'' here because Sublime Text 2 syntax highlight has a fit
REGEX_SAFETY = re.compile(r'([?\\\\.[\]()*+\^$!\|])')
# allowed field types
ALLOWED_TYPES = set(list('nbox%fFegwWdDsSl') + ['t' + c for c in 'ieahgcts'])
def extract_format(format, extra_types):
"""Pull apart the format [[fill]align][0][width][.precision][type]"""
fill = align = None
if format[0] in '<>=^':
align = format[0]
format = format[1:]
elif len(format) > 1 and format[1] in '<>=^':
fill = format[0]
align = format[1]
format = format[2:]
zero = False
if format and format[0] == '0':
zero = True
format = format[1:]
width = ''
while format:
if not format[0].isdigit():
break
width += format[0]
format = format[1:]
if format.startswith('.'):
# Precision isn't needed but we need to capture it so that
# the ValueError isn't raised.
format = format[1:] # drop the '.'
precision = ''
while format:
if not format[0].isdigit():
break
precision += format[0]
format = format[1:]
# the rest is the type, if present
type = format
if type and type not in ALLOWED_TYPES and type not in extra_types:
raise ValueError('format spec %r not recognised' % type)
return locals()
PARSE_RE = re.compile(r"""({{|}}|{\w*(?:(?:\.\w+)|(?:\[[^\]]+\]))*(?::[^}]+)?})""")
class Parser(object):
"""Encapsulate a format string that may be used to parse other strings."""
def __init__(self, format, extra_types=None, case_sensitive=False):
# a mapping of a name as in {hello.world} to a regex-group compatible
# name, like hello__world Its used to prevent the transformation of
# name-to-group and group to name to fail subtly, such as in:
# hello_.world-> hello___world->hello._world
self._group_to_name_map = {}
# also store the original field name to group name mapping to allow
# multiple instances of a name in the format string
self._name_to_group_map = {}
# and to sanity check the repeated instances store away the first
# field type specification for the named field
self._name_types = {}
self._format = format
if extra_types is None:
extra_types = {}
self._extra_types = extra_types
if case_sensitive:
self._re_flags = re.DOTALL
else:
self._re_flags = re.IGNORECASE | re.DOTALL
self._fixed_fields = []
self._named_fields = []
self._group_index = 0
self._type_conversions = {}
self._expression = self._generate_expression()
self.__search_re = None
self.__match_re = None
log.debug('format %r -> %r', format, self._expression)
def __repr__(self):
if len(self._format) > 20:
return '<%s %r>' % (self.__class__.__name__, self._format[:17] + '...')
return '<%s %r>' % (self.__class__.__name__, self._format)
@property
def _search_re(self):
if self.__search_re is None:
try:
self.__search_re = re.compile(self._expression, self._re_flags)
except AssertionError:
# access error through sys to keep py3k and backward compat
e = str(sys.exc_info()[1])
if e.endswith('this version only supports 100 named groups'):
raise TooManyFields(
'sorry, you are attempting to parse ' 'too many complex fields'
)
return self.__search_re
@property
def _match_re(self):
if self.__match_re is None:
expression = r'^%s$' % self._expression
try:
self.__match_re = re.compile(expression, self._re_flags)
except AssertionError:
# access error through sys to keep py3k and backward compat
e = str(sys.exc_info()[1])
if e.endswith('this version only supports 100 named groups'):
raise TooManyFields(
'sorry, you are attempting to parse ' 'too many complex fields'
)
except re.error:
raise NotImplementedError(
"Group names (e.g. (?P) can "
"cause failure, as they are not escaped properly: '%s'" % expression
)
return self.__match_re
@property
def named_fields(self):
return self._named_fields.copy()
@property
def fixed_fields(self):
return self._fixed_fields.copy()
def parse(self, string, evaluate_result=True):
"""Match my format to the string exactly.
Return a Result or Match instance or None if there's no match.
"""
m = self._match_re.match(string)
if m is None:
return None
if evaluate_result:
return self.evaluate_result(m)
else:
return Match(self, m)
def search(self, string, pos=0, endpos=None, evaluate_result=True):
"""Search the string for my format.
Optionally start the search at "pos" character index and limit the
search to a maximum index of endpos - equivalent to
search(string[:endpos]).
If the ``evaluate_result`` argument is set to ``False`` a
Match instance is returned instead of the actual Result instance.
Return either a Result instance or None if there's no match.
"""
if endpos is None:
endpos = len(string)
m = self._search_re.search(string, pos, endpos)
if m is None:
return None
if evaluate_result:
return self.evaluate_result(m)
else:
return Match(self, m)
def findall(
self, string, pos=0, endpos=None, extra_types=None, evaluate_result=True
):
"""Search "string" for all occurrences of "format".
Optionally start the search at "pos" character index and limit the
search to a maximum index of endpos - equivalent to
search(string[:endpos]).
Returns an iterator that holds Result or Match instances for each format match
found.
"""
if endpos is None:
endpos = len(string)
return ResultIterator(
self, string, pos, endpos, evaluate_result=evaluate_result
)
def _expand_named_fields(self, named_fields):
result = {}
for field, value in named_fields.items():
# split 'aaa[bbb][ccc]...' into 'aaa' and '[bbb][ccc]...'
basename, subkeys = re.match(r'([^\[]+)(.*)', field).groups()
# create nested dictionaries {'aaa': {'bbb': {'ccc': ...}}}
d = result
k = basename
if subkeys:
for subkey in re.findall(r'\[[^\]]+\]', subkeys):
d = d.setdefault(k, {})
k = subkey[1:-1]
# assign the value to the last key
d[k] = value
return result
def evaluate_result(self, m):
'''Generate a Result instance for the given regex match object'''
# ok, figure the fixed fields we've pulled out and type convert them
fixed_fields = list(m.groups())
for n in self._fixed_fields:
if n in self._type_conversions:
fixed_fields[n] = self._type_conversions[n](fixed_fields[n], m)
fixed_fields = tuple(fixed_fields[n] for n in self._fixed_fields)
# grab the named fields, converting where requested
groupdict = m.groupdict()
named_fields = {}
name_map = {}
for k in self._named_fields:
korig = self._group_to_name_map[k]
name_map[korig] = k
if k in self._type_conversions:
value = self._type_conversions[k](groupdict[k], m)
else:
value = groupdict[k]
named_fields[korig] = value
# now figure the match spans
spans = dict((n, m.span(name_map[n])) for n in named_fields)
spans.update((i, m.span(n + 1)) for i, n in enumerate(self._fixed_fields))
# and that's our result
return Result(fixed_fields, self._expand_named_fields(named_fields), spans)
def _regex_replace(self, match):
return '\\' + match.group(1)
def _generate_expression(self):
# turn my _format attribute into the _expression attribute
e = []
for part in PARSE_RE.split(self._format):
if not part:
continue
elif part == '{{':
e.append(r'\{')
elif part == '}}':
e.append(r'\}')
elif part[0] == '{' and part[-1] == '}':
# this will be a braces-delimited field to handle
e.append(self._handle_field(part))
else:
# just some text to match
e.append(REGEX_SAFETY.sub(self._regex_replace, part))
return ''.join(e)
def _to_group_name(self, field):
# return a version of field which can be used as capture group, even
# though it might contain '.'
group = field.replace('.', '_').replace('[', '_').replace(']', '_')
# make sure we don't collide ("a.b" colliding with "a_b")
n = 1
while group in self._group_to_name_map:
n += 1
if '.' in field:
group = field.replace('.', '_' * n)
elif '_' in field:
group = field.replace('_', '_' * n)
else:
raise KeyError('duplicated group name %r' % (field,))
# save off the mapping
self._group_to_name_map[group] = field
self._name_to_group_map[field] = group
return group
def _handle_field(self, field):
# first: lose the braces
field = field[1:-1]
# now figure whether this is an anonymous or named field, and whether
# there's any format specification
format = ''
if 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 r'(?P=%s)' % group
else:
group = self._to_group_name(name)
self._name_types[name] = format
self._named_fields.append(group)
# this will become a group, which must not contain dots
wrap = r'(?P<%s>%%s)' % group
else:
self._fixed_fields.append(self._group_index)
wrap = r'(%s)'
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 % r'.+?'
# decode the format specification
format = extract_format(format, self._extra_types)
# figure type conversions, if any
type = format['type']
is_numeric = type and type in 'n%fegdobx'
if type in self._extra_types:
type_converter = self._extra_types[type]
s = getattr(type_converter, 'pattern', r'.+?')
regex_group_count = getattr(type_converter, 'regex_group_count', 0)
if regex_group_count is None:
regex_group_count = 0
self._group_index += regex_group_count
self._type_conversions[group] = convert_first(type_converter)
elif type == 'n':
s = r'\d{1,3}([,.]\d{3})*'
self._group_index += 1
self._type_conversions[group] = int_convert(10)
elif type == 'b':
s = r'(0[bB])?[01]+'
self._type_conversions[group] = int_convert(2)
self._group_index += 1
elif type == 'o':
s = r'(0[oO])?[0-7]+'
self._type_conversions[group] = int_convert(8)
self._group_index += 1
elif type == 'x':
s = r'(0[xX])?[0-9a-fA-F]+'
self._type_conversions[group] = int_convert(16)
self._group_index += 1
elif type == '%':
s = r'\d+(\.\d+)?%'
self._group_index += 1
self._type_conversions[group] = percentage
elif type == 'f':
s = r'\d*\.\d+'
self._type_conversions[group] = convert_first(float)
elif type == 'F':
s = r'\d*\.\d+'
self._type_conversions[group] = convert_first(Decimal)
elif type == 'e':
s = r'\d*\.\d+[eE][-+]?\d+|nan|NAN|[-+]?inf|[-+]?INF'
self._type_conversions[group] = convert_first(float)
elif type == 'g':
s = r'\d+(\.\d+)?([eE][-+]?\d+)?|nan|NAN|[-+]?inf|[-+]?INF'
self._group_index += 2
self._type_conversions[group] = convert_first(float)
elif type == 'd':
if format.get('width'):
width = r'{1,%s}' % int(format['width'])
else:
width = '+'
s = r'\d{w}|[-+ ]?0[xX][0-9a-fA-F]{w}|[-+ ]?0[bB][01]{w}|[-+ ]?0[oO][0-7]{w}'.format(
w=width
)
self._type_conversions[
group
] = int_convert() # do not specify number base, determine it automatically
elif type == 'ti':
s = r'(\d{4}-\d\d-\d\d)((\s+|T)%s)?(Z|\s*[-+]\d\d:?\d\d)?' % TIME_PAT
n = self._group_index
self._type_conversions[group] = partial(
date_convert, ymd=n + 1, hms=n + 4, tz=n + 7
)
self._group_index += 7
elif type == 'tg':
s = r'(\d{1,2}[-/](\d{1,2}|%s)[-/]\d{4})(\s+%s)?%s?%s?' % (
ALL_MONTHS_PAT,
TIME_PAT,
AM_PAT,
TZ_PAT,
)
n = self._group_index
self._type_conversions[group] = partial(
date_convert, dmy=n + 1, hms=n + 5, am=n + 8, tz=n + 9
)
self._group_index += 9
elif type == 'ta':
s = r'((\d{1,2}|%s)[-/]\d{1,2}[-/]\d{4})(\s+%s)?%s?%s?' % (
ALL_MONTHS_PAT,
TIME_PAT,
AM_PAT,
TZ_PAT,
)
n = self._group_index
self._type_conversions[group] = partial(
date_convert, mdy=n + 1, hms=n + 5, am=n + 8, tz=n + 9
)
self._group_index += 9
elif type == 'te':
# this will allow microseconds through if they're present, but meh
s = r'(%s,\s+)?(\d{1,2}\s+%s\s+\d{4})\s+%s%s' % (
DAYS_PAT,
MONTHS_PAT,
TIME_PAT,
TZ_PAT,
)
n = self._group_index
self._type_conversions[group] = partial(
date_convert, dmy=n + 3, hms=n + 5, tz=n + 8
)
self._group_index += 8
elif type == 'th':
# slight flexibility here from the stock Apache format
s = r'(\d{1,2}[-/]%s[-/]\d{4}):%s%s' % (MONTHS_PAT, TIME_PAT, TZ_PAT)
n = self._group_index
self._type_conversions[group] = partial(
date_convert, dmy=n + 1, hms=n + 3, tz=n + 6
)
self._group_index += 6
elif type == 'tc':
s = r'(%s)\s+%s\s+(\d{1,2})\s+%s\s+(\d{4})' % (
DAYS_PAT,
MONTHS_PAT,
TIME_PAT,
)
n = self._group_index
self._type_conversions[group] = partial(
date_convert, d_m_y=(n + 4, n + 3, n + 8), hms=n + 5
)
self._group_index += 8
elif type == 'tt':
s = r'%s?%s?%s?' % (TIME_PAT, AM_PAT, TZ_PAT)
n = self._group_index
self._type_conversions[group] = partial(
date_convert, hms=n + 1, am=n + 4, tz=n + 5
)
self._group_index += 5
elif type == 'ts':
s = r'%s(\s+)(\d+)(\s+)(\d{1,2}:\d{1,2}:\d{1,2})?' % MONTHS_PAT
n = self._group_index
self._type_conversions[group] = partial(
date_convert, mm=n + 1, dd=n + 3, hms=n + 5
)
self._group_index += 5
elif type == 'l':
s = r'[A-Za-z]+'
elif type:
s = r'\%s+' % type
elif format.get('precision'):
if format.get('width'):
s = r'.{%s,%s}?' % (format['width'], format['precision'])
else:
s = r'.{1,%s}?' % format['precision']
elif format.get('width'):
s = r'.{%s,}?' % format['width']
else:
s = r'.+?'
align = format['align']
fill = format['fill']
# handle some numeric-specific things like fill and sign
if is_numeric:
# prefix with something (align "=" trumps zero)
if align == '=':
# special case - align "=" acts like the zero above but with
# configurable fill defaulting to "0"
if not fill:
fill = '0'
s = r'%s*' % fill + s
# allow numbers to be prefixed with a sign
s = r'[-+ ]?' + s
if not fill:
fill = ' '
# Place into a group now - this captures the value we want to keep.
# Everything else from now is just padding to be stripped off
if wrap:
s = wrap % s
self._group_index += 1
if format['width']:
# all we really care about is that if the format originally
# specified a width then there will probably be padding - without
# an explicit alignment that'll mean right alignment with spaces
# padding
if not align:
align = '>'
if fill in r'.\+?*[](){}^$':
fill = '\\' + fill
# align "=" has been handled
if align == '<':
s = '%s%s+' % (s, fill)
elif align == '>':
s = '%s*%s' % (fill, s)
elif align == '^':
s = '%s*%s%s+' % (fill, s, fill)
return s
class Result(object):
"""The result of a parse() or search().
Fixed results may be looked up using `result[index]`.
Named results may be looked up using `result['name']`.
Named results may be tested for existence using `'name' in result`.
"""
def __init__(self, fixed, named, spans):
self.fixed = fixed
self.named = named
self.spans = spans
def __getitem__(self, item):
if isinstance(item, int):
return self.fixed[item]
return self.named[item]
def __repr__(self):
return '<%s %r %r>' % (self.__class__.__name__, self.fixed, self.named)
def __contains__(self, name):
return name in self.named
class Match(object):
"""The result of a parse() or search() if no results are generated.
This class is only used to expose internal used regex match objects
to the user and use them for external Parser.evaluate_result calls.
"""
def __init__(self, parser, match):
self.parser = parser
self.match = match
def evaluate_result(self):
'''Generate results for this Match'''
return self.parser.evaluate_result(self.match)
class ResultIterator(object):
"""The result of a findall() operation.
Each element is a Result instance.
"""
def __init__(self, parser, string, pos, endpos, evaluate_result=True):
self.parser = parser
self.string = string
self.pos = pos
self.endpos = endpos
self.evaluate_result = evaluate_result
def __iter__(self):
return self
def __next__(self):
m = self.parser._search_re.search(self.string, self.pos, self.endpos)
if m is None:
raise StopIteration()
self.pos = m.end()
if self.evaluate_result:
return self.parser.evaluate_result(m)
else:
return Match(self.parser, m)
# pre-py3k compat
next = __next__
def parse(format, string, extra_types=None, evaluate_result=True, case_sensitive=False):
"""Using "format" attempt to pull values from "string".
The format must match the string contents exactly. If the value
you're looking for is instead just a part of the string use
search().
If ``evaluate_result`` is True the return value will be an Result instance with two attributes:
.fixed - tuple of fixed-position values from the string
.named - dict of named values from the string
If ``evaluate_result`` is False the return value will be a Match instance with one method:
.evaluate_result() - This will return a Result instance like you would get
with ``evaluate_result`` set to True
The default behaviour is to match strings case insensitively. You may match with
case by specifying case_sensitive=True.
If the format is invalid a ValueError will be raised.
See the module documentation for the use of "extra_types".
In the case there is no match parse() will return None.
"""
p = Parser(format, extra_types=extra_types, case_sensitive=case_sensitive)
return p.parse(string, evaluate_result=evaluate_result)
def search(
format,
string,
pos=0,
endpos=None,
extra_types=None,
evaluate_result=True,
case_sensitive=False,
):
"""Search "string" for the first occurrence of "format".
The format may occur anywhere within the string. If
instead you wish for the format to exactly match the string
use parse().
Optionally start the search at "pos" character index and limit the search
to a maximum index of endpos - equivalent to search(string[:endpos]).
If ``evaluate_result`` is True the return value will be an Result instance with two attributes:
.fixed - tuple of fixed-position values from the string
.named - dict of named values from the string
If ``evaluate_result`` is False the return value will be a Match instance with one method:
.evaluate_result() - This will return a Result instance like you would get
with ``evaluate_result`` set to True
The default behaviour is to match strings case insensitively. You may match with
case by specifying case_sensitive=True.
If the format is invalid a ValueError will be raised.
See the module documentation for the use of "extra_types".
In the case there is no match parse() will return None.
"""
p = Parser(format, extra_types=extra_types, case_sensitive=case_sensitive)
return p.search(string, pos, endpos, evaluate_result=evaluate_result)
def findall(
format,
string,
pos=0,
endpos=None,
extra_types=None,
evaluate_result=True,
case_sensitive=False,
):
"""Search "string" for all occurrences of "format".
You will be returned an iterator that holds Result instances
for each format match found.
Optionally start the search at "pos" character index and limit the search
to a maximum index of endpos - equivalent to search(string[:endpos]).
If ``evaluate_result`` is True each returned Result instance has two attributes:
.fixed - tuple of fixed-position values from the string
.named - dict of named values from the string
If ``evaluate_result`` is False each returned value is a Match instance with one method:
.evaluate_result() - This will return a Result instance like you would get
with ``evaluate_result`` set to True
The default behaviour is to match strings case insensitively. You may match with
case by specifying case_sensitive=True.
If the format is invalid a ValueError will be raised.
See the module documentation for the use of "extra_types".
"""
p = Parser(format, extra_types=extra_types, case_sensitive=case_sensitive)
return p.findall(string, pos, endpos, evaluate_result=evaluate_result)
def compile(format, extra_types=None, case_sensitive=False):
"""Create a Parser instance to parse "format".
The resultant Parser has a method .parse(string) which
behaves in the same manner as parse(format, string).
The default behaviour is to match strings case insensitively. You may match with
case by specifying case_sensitive=True.
Use this function if you intend to parse many strings
with the same format.
See the module documentation for the use of "extra_types".
Returns a Parser instance.
"""
return Parser(format, extra_types=extra_types, case_sensitive=case_sensitive)
# Copyright (c) 2012-2020 Richard Jones
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# vim: set filetype=python ts=4 sw=4 et si tw=75
parse_type-0.5.6/parse_type/parse_util.py 0000664 0000000 0000000 00000014040 13726616614 0020604 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
# pylint: disable=missing-docstring
"""
Provides generic utility classes for the :class:`parse.Parser` class.
"""
from __future__ import absolute_import
from collections import namedtuple
import parse
import six
# -- HELPER-CLASS: For format part in a Field.
# REQUIRES: Python 2.6 or newer.
# pylint: disable=redefined-builtin, too-many-arguments
FormatSpec = namedtuple("FormatSpec",
["type", "width", "zero", "align", "fill", "precision"])
def make_format_spec(type=None, width="", zero=False, align=None, fill=None,
precision=None):
return FormatSpec(type, width, zero, align, fill, precision)
# pylint: enable=redefined-builtin
class Field(object):
"""
Provides a ValueObject for a Field in a parse expression.
Examples:
* "{}"
* "{name}"
* "{:format}"
* "{name:format}"
Format specification: [[fill]align][0][width][.precision][type]
"""
# pylint: disable=redefined-builtin
ALIGN_CHARS = '<>=^'
def __init__(self, name="", format=None):
self.name = name
self.format = format
self._format_spec = None
def set_format(self, format):
self.format = format
self._format_spec = None
@property
def has_format(self):
return bool(self.format)
@property
def format_spec(self):
if not self._format_spec and self.format:
self._format_spec = self.extract_format_spec(self.format)
return self._format_spec
def __str__(self):
name = self.name or ""
if self.has_format:
return "{%s:%s}" % (name, self.format)
return "{%s}" % name
def __eq__(self, other):
if isinstance(other, Field):
format1 = self.format or ""
format2 = other.format or ""
return (self.name == other.name) and (format1 == format2)
elif isinstance(other, six.string_types):
return str(self) == other
else:
raise ValueError(other)
def __ne__(self, other):
return not self.__eq__(other)
@staticmethod
def make_format(format_spec):
"""Build format string from a format specification.
:param format_spec: Format specification (as FormatSpec object).
:return: Composed format (as string).
"""
fill = ''
align = ''
zero = ''
width = format_spec.width
if format_spec.align:
align = format_spec.align[0]
if format_spec.fill:
fill = format_spec.fill[0]
if format_spec.zero:
zero = '0'
precision_part = ""
if format_spec.precision:
precision_part = ".%s" % format_spec.precision
# -- FORMAT-SPEC: [[fill]align][0][width][.precision][type]
return "%s%s%s%s%s%s" % (fill, align, zero, width,
precision_part, format_spec.type)
@classmethod
def extract_format_spec(cls, format):
"""Pull apart the format: [[fill]align][0][width][.precision][type]"""
# -- BASED-ON: parse.extract_format()
# pylint: disable=redefined-builtin, unsubscriptable-object
if not format:
raise ValueError("INVALID-FORMAT: %s (empty-string)" % format)
orig_format = format
fill = align = None
if format[0] in cls.ALIGN_CHARS:
align = format[0]
format = format[1:]
elif len(format) > 1 and format[1] in cls.ALIGN_CHARS:
fill = format[0]
align = format[1]
format = format[2:]
zero = False
if format and format[0] == '0':
zero = True
format = format[1:]
width = ''
while format:
if not format[0].isdigit():
break
width += format[0]
format = format[1:]
precision = None
if format.startswith('.'):
# Precision isn't needed but we need to capture it so that
# the ValueError isn't raised.
format = format[1:] # drop the '.'
precision = ''
while format:
if not format[0].isdigit():
break
precision += format[0]
format = format[1:]
# the rest is the type, if present
type = format
if not type:
raise ValueError("INVALID-FORMAT: %s (without type)" % orig_format)
return FormatSpec(type, width, zero, align, fill, precision)
class FieldParser(object):
"""
Utility class that parses/extracts fields in parse expressions.
"""
@classmethod
def parse(cls, text):
if not (text.startswith('{') and text.endswith('}')):
message = "FIELD-SCHEMA MISMATCH: text='%s' (missing braces)" % text
raise ValueError(message)
# first: lose the braces
text = text[1:-1]
if ':' in text:
# -- CASE: Typed field with format.
name, format_ = text.split(':')
else:
name = text
format_ = None
return Field(name, format_)
@classmethod
def extract_fields(cls, schema):
"""Extract fields in a parse expression schema.
:param schema: Parse expression schema/format to use (as string).
:return: Generator for fields in schema (as Field objects).
"""
# -- BASED-ON: parse.Parser._generate_expression()
for part in parse.PARSE_RE.split(schema):
if not part or part == '{{' or part == '}}':
continue
elif part[0] == '{':
# this will be a braces-delimited field to handle
yield cls.parse(part)
@classmethod
def extract_types(cls, schema):
"""Extract types (names) for typed fields (with format/type part).
:param schema: Parser schema/format to use.
:return: Generator for type names (as string).
"""
for field in cls.extract_fields(schema):
if field.has_format:
yield field.format_spec.type
parse_type-0.5.6/py.requirements/ 0000775 0000000 0000000 00000000000 13726616614 0017063 5 ustar 00root root 0000000 0000000 parse_type-0.5.6/py.requirements/all.txt 0000664 0000000 0000000 00000000702 13726616614 0020373 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
-r testing.txt
-r py26_more.txt
parse_type-0.5.6/py.requirements/basic.txt 0000664 0000000 0000000 00000000665 13726616614 0020714 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.8.4
enum34
six >= 1.11.0
parse_type-0.5.6/py.requirements/ci.travis.txt 0000664 0000000 0000000 00000000655 13726616614 0021534 0 ustar 00root root 0000000 0000000 pytest < 5.0; python_version < '3.0'
pytest >= 5.0; python_version >= '3.0'
pytest-html >= 1.19.0
unittest2; python_version < '2.7'
ordereddict; python_version < '2.7'
# -- NEEDED: By some tests (as proof of concept)
# NOTE: path.py-10.1 is required for python2.6
# HINT: path.py => path (python-install-package was renamed for python3)
path.py >= 11.5.0; python_version < '3.5'
path >= 13.1.0; python_version >= '3.5'
parse_type-0.5.6/py.requirements/develop.txt 0000664 0000000 0000000 00000001445 13726616614 0021266 0 ustar 00root root 0000000 0000000 # ============================================================================
# PYTHON PACKAGE REQUIREMENTS FOR: parse_type -- For development only
# ============================================================================
# -- DEVELOPMENT SUPPORT:
invoke >= 1.2.0
six >= 1.11.0
pathlib; python_version <= '3.4'
# -- HINT: path.py => path (python-install-package was renamed for python3)
path.py >= 11.5.0; python_version < '3.5'
path >= 13.1.0; python_version >= '3.5'
# For cleanup of python files: py.cleanup
pycmd
# -- PROJECT ADMIN SUPPORT:
# OLD: bumpversion
bump2version >= 0.5.6
# -- RELEASE MANAGEMENT: Push package to pypi.
twine >= 1.13.0
# -- PYTHON2/PYTHON3 COMPATIBILITY:
modernize >= 0.5
pylint
# -- RELATED:
-r testing.txt
-r docs.txt
# -- DISABLED:
# -r optional.txt
parse_type-0.5.6/py.requirements/docs.txt 0000664 0000000 0000000 00000000413 13726616614 0020552 0 ustar 00root root 0000000 0000000 # ============================================================================
# PYTHON PACKAGE REQUIREMENTS: For documentation generation
# ============================================================================
# sphinxcontrib-cheeseshop >= 0.2
Sphinx >= 1.5
parse_type-0.5.6/py.requirements/optional.txt 0000664 0000000 0000000 00000000575 13726616614 0021460 0 ustar 00root root 0000000 0000000 # ============================================================================
# PYTHON PACKAGE REQUIREMENTS FOR: parse_type -- Optional for development
# ============================================================================
# -- GIT MULTI-REPO TOOL: wstool
# REQUIRES: wstool >= 0.1.18 (which is not in pypi.org, yet)
https://github.com/vcstools/wstool/archive/0.1.18.zip
parse_type-0.5.6/py.requirements/py26_more.txt 0000664 0000000 0000000 00000000046 13726616614 0021446 0 ustar 00root root 0000000 0000000 ordereddict; python_version <= '2.6'
parse_type-0.5.6/py.requirements/testing.txt 0000664 0000000 0000000 00000000637 13726616614 0021307 0 ustar 00root root 0000000 0000000 # ============================================================================
# PYTHON PACKAGE REQUIREMENTS FOR: parse_type -- For testing only
# ============================================================================
pytest >= 4.2
pytest-html >= 1.16
pytest-cov
pytest-runner
# -- PYTHON 2.6 SUPPORT:
unittest2; python_version <= '2.6'
tox >= 2.8
coverage >= 4.4
# -- NEEDED-FOR: toxcmd.py
argparse
parse_type-0.5.6/pytest.ini 0000664 0000000 0000000 00000002200 13726616614 0015734 0 ustar 00root root 0000000 0000000 # ============================================================================
# PYTEST CONFIGURATION FILE: pytest.ini
# ============================================================================
# SEE ALSO:
# * http://pytest.org/
# * http://pytest.org/latest/customize.html
# * http://pytest.org/latest/usage.html
# * http://pytest.org/latest/example/pythoncollection.html#change-naming-conventions
# ============================================================================
# MORE OPTIONS:
# addopts =
# python_classes=*Test
# python_functions=test
# ============================================================================
[pytest]
minversion = 4.2
testpaths = tests
python_files = test_*.py
junit_family = xunit2
addopts = --metadata PACKAGE_UNDER_TEST parse_type
--metadata PACKAGE_VERSION 0.5.6
--html=build/testing/report.html --self-contained-html
--junit-xml=build/testing/report.xml
# markers =
# smoke
# slow
# -- PREPARED:
# filterwarnings =
# ignore:.*invalid escape sequence.*:DeprecationWarning
# -- BACKWARD COMPATIBILITY: pytest < 2.8
norecursedirs = .git .tox build dist .venv* tmp* _*
parse_type-0.5.6/setup.cfg 0000664 0000000 0000000 00000000627 13726616614 0015537 0 ustar 00root root 0000000 0000000 # -- CONVENIENCE: Use pytest-runner (ptr) as test runner.
[aliases]
docs = build_sphinx
test = pytest
[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
[upload_docs]
upload-dir = build/docs/html
[bdist_wheel]
universal = 1
parse_type-0.5.6/setup.py 0000664 0000000 0000000 00000010622 13726616614 0015424 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Setup script for "parse_type" package.
USAGE:
python setup.py install
# OR:
pip install .
SEE ALSO:
* https://pypi.org/pypi/parse_type
* https://github.com/jenisys/parse_type
RELATED:
* https://setuptools.readthedocs.io/en/latest/history.html
"""
import sys
import os.path
sys.path.insert(0, os.curdir)
# -- USE: setuptools
from setuptools import setup, find_packages
# -----------------------------------------------------------------------------
# PREPARE SETUP:
# -----------------------------------------------------------------------------
HERE = os.path.dirname(__file__)
python_version = float('%s.%s' % sys.version_info[:2])
README = os.path.join(HERE, "README.rst")
long_description = ''.join(open(README).readlines()[4:])
extra = dict(
tests_require=[
"pytest < 5.0; python_version < '3.0'", # >= 4.2
"pytest >= 5.0; python_version >= '3.0'",
"pytest-html >= 1.19.0",
# -- PYTHON 2.6 SUPPORT:
"unittest2; python_version < '2.7'",
],
)
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"
USE_PYTEST_RUNNER = os.environ.get("PYSETUP_TEST", "no") == "pytest"
if USE_PYTEST_RUNNER:
extra["tests_require"].append("pytest-runner")
# -----------------------------------------------------------------------------
# UTILITY:
# -----------------------------------------------------------------------------
def find_packages_by_root_package(where):
"""
Better than excluding everything that is not needed,
collect only what is needed.
"""
root_package = os.path.basename(where)
packages = [ "%s.%s" % (root_package, sub_package)
for sub_package in find_packages(where)]
packages.insert(0, root_package)
return packages
# -----------------------------------------------------------------------------
# SETUP:
# -----------------------------------------------------------------------------
setup(
name = "parse_type",
version = "0.5.6",
author = "Jens Engel",
author_email = "jenisys@noreply.github.com",
url = "https://github.com/jenisys/parse_type",
download_url= "http://pypi.python.org/pypi/parse_type",
description = "Simplifies to build parse types based on the parse module",
long_description = long_description,
keywords= "parse, parsing",
license = "BSD",
packages = find_packages_by_root_package("parse_type"),
include_package_data = True,
# -- REQUIREMENTS:
python_requires=">=2.7, !=3.0.*, !=3.1.*",
install_requires=[
"parse >= 1.18.0; python_version >= '3.0'",
"parse >= 1.13.1; python_version <= '2.7'",
# -- MAYBE, related to issue #15:
# "parse == 1.13.1; python_version <= '2.7'",
"enum34; python_version < '3.4'",
"six >= 1.11",
"ordereddict; python_version < '2.7'",
],
extras_require={
'docs': ["sphinx>=1.2"],
'develop': [
"coverage >= 4.4",
"pytest < 5.0; python_version < '3.0'", # >= 4.2
"pytest >= 5.0; python_version >= '3.0'",
"pytest-html >= 1.19.0",
"pytest-cov",
"tox >= 2.8",
],
},
test_suite = "tests",
test_loader = "setuptools.command.test:ScanningLoader",
zip_safe = True,
classifiers = [
"Development Status :: 4 - Beta",
"Environment :: Console",
"Environment :: Web Environment",
"Intended Audience :: Developers",
"Operating System :: OS Independent",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3.2",
"Programming Language :: Python :: 3.3",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 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.5.6/tasks/ 0000775 0000000 0000000 00000000000 13726616614 0015036 5 ustar 00root root 0000000 0000000 parse_type-0.5.6/tasks/__init__.py 0000664 0000000 0000000 00000004366 13726616614 0017160 0 ustar 00root root 0000000 0000000 # -*- coding: UTF-8 -*-
# pylint: disable=wrong-import-position, wrong-import-order
"""
Invoke build script.
Show all tasks with::
invoke -l
.. seealso::
* http://pyinvoke.org
* https://github.com/pyinvoke/invoke
"""
from __future__ import absolute_import
# -----------------------------------------------------------------------------
# BOOTSTRAP PATH: Use provided vendor bundle if "invoke" is not installed
# -----------------------------------------------------------------------------
from . import _setup # pylint: disable=wrong-import-order
import os.path
import sys
INVOKE_MINVERSION = "1.2.0"
_setup.setup_path()
_setup.require_invoke_minversion(INVOKE_MINVERSION)
# -----------------------------------------------------------------------------
# IMPORTS:
# -----------------------------------------------------------------------------
import sys
from invoke import Collection
# -- TASK-LIBRARY:
from . import _tasklet_cleanup as cleanup
from . import test
from . import release
# DISABLED: from . import docs
# -----------------------------------------------------------------------------
# TASKS:
# -----------------------------------------------------------------------------
# None
# -----------------------------------------------------------------------------
# TASK CONFIGURATION:
# -----------------------------------------------------------------------------
namespace = Collection()
namespace.add_collection(Collection.from_module(cleanup), name="cleanup")
namespace.add_collection(Collection.from_module(test))
namespace.add_collection(Collection.from_module(release))
# -- DISABLED: namespace.add_collection(Collection.from_module(docs))
namespace.configure({
"tasks": {
"auto_dash_names": False
}
})
# -- ENSURE: python cleanup is used for this project.
cleanup.cleanup_tasks.add_task(cleanup.clean_python)
# -- INJECT: clean configuration into this namespace
namespace.configure(cleanup.namespace.configuration())
if sys.platform.startswith("win"):
# -- OVERRIDE SETTINGS: For platform=win32, ... (Windows)
from ._compat_shutil import which
run_settings = dict(echo=True, pty=False, shell=which("cmd"))
namespace.configure({"run": run_settings})
else:
namespace.configure({"run": dict(echo=True, pty=True)})
parse_type-0.5.6/tasks/__main__.py 0000664 0000000 0000000 00000003552 13726616614 0017135 0 ustar 00root root 0000000 0000000 # -*- coding: UTF-8 -*-
"""
Provides "invoke" script when invoke is not installed.
Note that this approach uses the "tasks/_vendor/invoke.zip" bundle package.
Usage::
# -- INSTEAD OF: invoke command
# Show invoke version
python -m tasks --version
# List all tasks
python -m tasks -l
.. seealso::
* http://pyinvoke.org
* https://github.com/pyinvoke/invoke
Examples for Invoke Scripts using the Bundle
-------------------------------------------------------------------------------
For UNIX like platforms:
.. code-block:: sh
#!/bin/sh
#!/bin/bash
# RUN INVOKE: From bundled ZIP file (with Bourne shell/bash script).
# FILE: invoke.sh (in directory that contains tasks/ directory)
HERE=$(dirname $0)
export INVOKE_TASKS_USE_VENDOR_BUNDLES="yes"
python ${HERE}/tasks/_vendor/invoke.zip $*
For Windows platform:
.. code-block:: bat
@echo off
REM RUN INVOKE: From bundled ZIP file (with Windows Batchfile).
REM FILE: invoke.cmd (in directory that contains tasks/ directory)
setlocal
set HERE=%~dp0
set INVOKE_TASKS_USE_VENDOR_BUNDLES="yes"
if not defined PYTHON set PYTHON=python
%PYTHON% %HERE%tasks/_vendor/invoke.zip "%*"
"""
from __future__ import absolute_import
import os
import sys
# -----------------------------------------------------------------------------
# BOOTSTRAP PATH: Use provided vendor bundle if "invoke" is not installed
# -----------------------------------------------------------------------------
# NOTE: tasks/__init__.py performs sys.path setup.
os.environ["INVOKE_TASKS_USE_VENDOR_BUNDLES"] = "yes"
# -----------------------------------------------------------------------------
# AUTO-MAIN:
# -----------------------------------------------------------------------------
if __name__ == "__main__":
from invoke.main import program
sys.exit(program.run())
parse_type-0.5.6/tasks/_compat_shutil.py 0000664 0000000 0000000 00000000334 13726616614 0020422 0 ustar 00root root 0000000 0000000 # -*- coding: UTF-8 -*-
# pylint: disable=unused-import
# PYTHON VERSION COMPATIBILITY HELPER
try:
from shutil import which # -- SINCE: Python 3.3
except ImportError:
from backports.shutil_which import which
parse_type-0.5.6/tasks/_dry_run.py 0000664 0000000 0000000 00000002205 13726616614 0017230 0 ustar 00root root 0000000 0000000 # -*- coding: UTF-8 -*-
"""
Basic support to use a --dry-run mode w/ invoke tasks.
.. code-block::
from ._dry_run import DryRunContext
@task
def destroy_something(ctx, path, dry_run=False):
if dry_run:
ctx = DryRunContext(ctx)
# -- DRY-RUN MODE: Only echos commands.
ctx.run("rm -rf {}".format(path))
"""
from __future__ import print_function
class DryRunContext(object):
PREFIX = "DRY-RUN: "
SCHEMA = "{prefix}{command}"
SCHEMA_WITH_KWARGS = "{prefix}{command} (with kwargs={kwargs})"
def __init__(self, ctx=None, prefix=None, schema=None):
if prefix is None:
prefix = self.PREFIX
if schema is None:
schema = self.SCHEMA
self.ctx = ctx
self.prefix = prefix
self.schema = schema
def run(self, command, **kwargs):
message = self.schema.format(command=command,
prefix=self.prefix,
kwargs=kwargs)
print(message)
def sudo(self, command, **kwargs):
command2 = "sudo %s" % command
self.run(command2, **kwargs)
parse_type-0.5.6/tasks/_setup.py 0000664 0000000 0000000 00000011247 13726616614 0016714 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
"""
Decides if vendor bundles are used or not.
Setup python path accordingly.
"""
from __future__ import absolute_import, print_function
import os.path
import sys
# -----------------------------------------------------------------------------
# DEFINES:
# -----------------------------------------------------------------------------
HERE = os.path.dirname(__file__)
TASKS_VENDOR_DIR = os.path.join(HERE, "_vendor")
INVOKE_BUNDLE = os.path.join(TASKS_VENDOR_DIR, "invoke.zip")
INVOKE_BUNDLE_VERSION = "1.2.0"
DEBUG_SYSPATH = False
# -----------------------------------------------------------------------------
# EXCEPTIONS:
# -----------------------------------------------------------------------------
class VersionRequirementError(SystemExit):
pass
# -----------------------------------------------------------------------------
# FUNCTIONS:
# -----------------------------------------------------------------------------
def setup_path(invoke_minversion=None):
"""Setup python search and add ``TASKS_VENDOR_DIR`` (if available)."""
# print("INVOKE.tasks: setup_path")
if not os.path.isdir(TASKS_VENDOR_DIR):
print("SKIP: TASKS_VENDOR_DIR=%s is missing" % TASKS_VENDOR_DIR)
return
elif os.path.abspath(TASKS_VENDOR_DIR) in sys.path:
# -- SETUP ALREADY DONE:
# return
pass
use_vendor_bundles = os.environ.get("INVOKE_TASKS_USE_VENDOR_BUNDLES", "no")
if need_vendor_bundles(invoke_minversion):
use_vendor_bundles = "yes"
if use_vendor_bundles == "yes":
syspath_insert(0, os.path.abspath(TASKS_VENDOR_DIR))
if setup_path_for_bundle(INVOKE_BUNDLE, pos=1):
import invoke
bundle_path = os.path.relpath(INVOKE_BUNDLE, os.getcwd())
print("USING: %s (version: %s)" % (bundle_path, invoke.__version__))
else:
# -- BEST-EFFORT: May rescue something
syspath_append(os.path.abspath(TASKS_VENDOR_DIR))
setup_path_for_bundle(INVOKE_BUNDLE, pos=len(sys.path))
if DEBUG_SYSPATH:
for index, p in enumerate(sys.path):
print(" %d. %s" % (index, p))
def require_invoke_minversion(min_version, verbose=False):
"""Ensures that :mod:`invoke` has at the least the :param:`min_version`.
Otherwise,
:param min_version: Minimal acceptable invoke version (as string).
:param verbose: Indicates if invoke.version should be shown.
:raises: VersionRequirementError=SystemExit if requirement fails.
"""
# -- REQUIRES: sys.path is setup and contains invoke
try:
import invoke
invoke_version = invoke.__version__
except ImportError:
invoke_version = "__NOT_INSTALLED"
if invoke_version < min_version:
message = "REQUIRE: invoke.version >= %s (but was: %s)" % \
(min_version, invoke_version)
message += "\nUSE: pip install invoke>=%s" % min_version
raise VersionRequirementError(message)
# pylint: disable=invalid-name
INVOKE_VERSION = os.environ.get("INVOKE_VERSION", None)
if verbose and not INVOKE_VERSION:
os.environ["INVOKE_VERSION"] = invoke_version
print("USING: invoke.version=%s" % invoke_version)
def need_vendor_bundles(invoke_minversion=None):
invoke_minversion = invoke_minversion or "0.0.0"
need_vendor_answers = []
need_vendor_answers.append(need_vendor_bundle_invoke(invoke_minversion))
# -- REQUIRE: path.py
try:
import path
need_bundle = False
except ImportError:
need_bundle = True
need_vendor_answers.append(need_bundle)
# -- DIAG: print("INVOKE: need_bundle=%s" % need_bundle1)
# return need_bundle1 or need_bundle2
return any(need_vendor_answers)
def need_vendor_bundle_invoke(invoke_minversion="0.0.0"):
# -- REQUIRE: invoke
try:
import invoke
need_bundle = invoke.__version__ < invoke_minversion
if need_bundle:
del sys.modules["invoke"]
del invoke
except ImportError:
need_bundle = True
except Exception: # pylint: disable=broad-except
need_bundle = True
return need_bundle
# -----------------------------------------------------------------------------
# UTILITY FUNCTIONS:
# -----------------------------------------------------------------------------
def setup_path_for_bundle(bundle_path, pos=0):
if os.path.exists(bundle_path):
syspath_insert(pos, os.path.abspath(bundle_path))
return True
return False
def syspath_insert(pos, path):
if path in sys.path:
sys.path.remove(path)
sys.path.insert(pos, path)
def syspath_append(path):
if path in sys.path:
sys.path.remove(path)
sys.path.append(path)
parse_type-0.5.6/tasks/_tasklet_cleanup.py 0000664 0000000 0000000 00000025220 13726616614 0020726 0 ustar 00root root 0000000 0000000 # -*- coding: UTF-8 -*-
"""
Provides cleanup tasks for invoke build scripts (as generic invoke tasklet).
Simplifies writing common, composable and extendable cleanup tasks.
PYTHON PACKAGE REQUIREMENTS:
* path.py >= 8.2.1 (as path-object abstraction)
* pathlib (for ant-like wildcard patterns; since: python > 3.5)
* pycmd (required-by: clean_python())
clean task: Add Additional Directories and Files to be removed
-------------------------------------------------------------------------------
Create an invoke configuration file (YAML of JSON) with the additional
configuration data:
.. code-block:: yaml
# -- FILE: invoke.yaml
# USE: clean.directories, clean.files to override current configuration.
clean:
extra_directories:
- **/tmp/
extra_files:
- **/*.log
- **/*.bak
Registration of Cleanup Tasks
------------------------------
Other task modules often have an own cleanup task to recover the clean state.
The :meth:`clean` task, that is provided here, supports the registration
of additional cleanup tasks. Therefore, when the :meth:`clean` task is executed,
all registered cleanup tasks will be executed.
EXAMPLE::
# -- FILE: tasks/docs.py
from __future__ import absolute_import
from invoke import task, Collection
from tasklet_cleanup import cleanup_tasks, cleanup_dirs
@task
def clean(ctx, dry_run=False):
"Cleanup generated documentation artifacts."
cleanup_dirs(["build/docs"])
namespace = Collection(clean)
...
# -- REGISTER CLEANUP TASK:
cleanup_tasks.add_task(clean, "clean_docs")
cleanup_tasks.configure(namespace.configuration())
"""
from __future__ import absolute_import, print_function
import os.path
import sys
import pathlib
from invoke import task, Collection
from invoke.executor import Executor
from invoke.exceptions import Exit, Failure, UnexpectedExit
from path import Path
# -----------------------------------------------------------------------------
# CLEANUP UTILITIES:
# -----------------------------------------------------------------------------
def cleanup_accept_old_config(ctx):
ctx.cleanup.directories.extend(ctx.clean.directories or [])
ctx.cleanup.extra_directories.extend(ctx.clean.extra_directories or [])
ctx.cleanup.files.extend(ctx.clean.files or [])
ctx.cleanup.extra_files.extend(ctx.clean.extra_files or [])
ctx.cleanup_all.directories.extend(ctx.clean_all.directories or [])
ctx.cleanup_all.extra_directories.extend(ctx.clean_all.extra_directories or [])
ctx.cleanup_all.files.extend(ctx.clean_all.files or [])
ctx.cleanup_all.extra_files.extend(ctx.clean_all.extra_files or [])
def execute_cleanup_tasks(ctx, cleanup_tasks, dry_run=False):
"""Execute several cleanup tasks as part of the cleanup.
REQUIRES: ``clean(ctx, dry_run=False)`` signature in cleanup tasks.
:param ctx: Context object for the tasks.
:param cleanup_tasks: Collection of cleanup tasks (as Collection).
:param dry_run: Indicates dry-run mode (bool)
"""
# pylint: disable=redefined-outer-name
executor = Executor(cleanup_tasks, ctx.config)
failure_count = 0
for cleanup_task in cleanup_tasks.tasks:
try:
print("CLEANUP TASK: %s" % cleanup_task)
executor.execute((cleanup_task, dict(dry_run=dry_run)))
except (Exit, Failure, UnexpectedExit) as e:
print("FAILURE in CLEANUP TASK: %s (GRACEFULLY-IGNORED)" % cleanup_task)
failure_count += 1
if failure_count:
print("CLEANUP TASKS: %d failure(s) occured" % failure_count)
def cleanup_dirs(patterns, dry_run=False, workdir="."):
"""Remove directories (and their contents) recursively.
Skips removal if directories does not exist.
:param patterns: Directory name patterns, like "**/tmp*" (as list).
:param dry_run: Dry-run mode indicator (as bool).
:param workdir: Current work directory (default=".")
"""
current_dir = Path(workdir)
python_basedir = Path(Path(sys.executable).dirname()).joinpath("..").abspath()
warn2_counter = 0
for dir_pattern in patterns:
for directory in path_glob(dir_pattern, current_dir):
directory2 = directory.abspath()
if sys.executable.startswith(directory2):
# pylint: disable=line-too-long
print("SKIP-SUICIDE: '%s' contains current python executable" % directory)
continue
elif directory2.startswith(python_basedir):
# -- PROTECT CURRENTLY USED VIRTUAL ENVIRONMENT:
if warn2_counter <= 4:
print("SKIP-SUICIDE: '%s'" % directory)
warn2_counter += 1
continue
if not directory.isdir():
print("RMTREE: %s (SKIPPED: Not a directory)" % directory)
continue
if dry_run:
print("RMTREE: %s (dry-run)" % directory)
else:
print("RMTREE: %s" % directory)
directory.rmtree_p()
def cleanup_files(patterns, dry_run=False, workdir="."):
"""Remove files or files selected by file patterns.
Skips removal if file does not exist.
:param patterns: File patterns, like "**/*.pyc" (as list).
:param dry_run: Dry-run mode indicator (as bool).
:param workdir: Current work directory (default=".")
"""
current_dir = Path(workdir)
python_basedir = Path(Path(sys.executable).dirname()).joinpath("..").abspath()
error_message = None
error_count = 0
for file_pattern in patterns:
for file_ in path_glob(file_pattern, current_dir):
if file_.abspath().startswith(python_basedir):
# -- PROTECT CURRENTLY USED VIRTUAL ENVIRONMENT:
continue
if not file_.isfile():
print("REMOVE: %s (SKIPPED: Not a file)" % file_)
continue
if dry_run:
print("REMOVE: %s (dry-run)" % file_)
else:
print("REMOVE: %s" % file_)
try:
file_.remove_p()
except os.error as e:
message = "%s: %s" % (e.__class__.__name__, e)
print(message + " basedir: "+ python_basedir)
error_count += 1
if not error_message:
error_message = message
if False and error_message:
class CleanupError(RuntimeError):
pass
raise CleanupError(error_message)
def path_glob(pattern, current_dir=None):
"""Use pathlib for ant-like patterns, like: "**/*.py"
:param pattern: File/directory pattern to use (as string).
:param current_dir: Current working directory (as Path, pathlib.Path, str)
:return Resolved Path (as path.Path).
"""
if not current_dir:
current_dir = pathlib.Path.cwd()
elif not isinstance(current_dir, pathlib.Path):
# -- CASE: string, path.Path (string-like)
current_dir = pathlib.Path(str(current_dir))
for p in current_dir.glob(pattern):
yield Path(str(p))
# -----------------------------------------------------------------------------
# GENERIC CLEANUP TASKS:
# -----------------------------------------------------------------------------
@task
def clean(ctx, dry_run=False):
"""Cleanup temporary dirs/files to regain a clean state."""
cleanup_accept_old_config(ctx)
directories = ctx.cleanup.directories or []
directories.extend(ctx.cleanup.extra_directories or [])
files = ctx.cleanup.files or []
files.extend(ctx.cleanup.extra_files or [])
# -- PERFORM CLEANUP:
execute_cleanup_tasks(ctx, cleanup_tasks, dry_run=dry_run)
cleanup_dirs(directories, dry_run=dry_run)
cleanup_files(files, dry_run=dry_run)
@task(name="all", aliases=("distclean",))
def clean_all(ctx, dry_run=False):
"""Clean up everything, even the precious stuff.
NOTE: clean task is executed first.
"""
cleanup_accept_old_config(ctx)
directories = ctx.config.cleanup_all.directories or []
directories.extend(ctx.config.cleanup_all.extra_directories or [])
files = ctx.config.cleanup_all.files or []
files.extend(ctx.config.cleanup_all.extra_files or [])
# -- PERFORM CLEANUP:
# HINT: Remove now directories, files first before cleanup-tasks.
cleanup_dirs(directories, dry_run=dry_run)
cleanup_files(files, dry_run=dry_run)
execute_cleanup_tasks(ctx, cleanup_all_tasks, dry_run=dry_run)
clean(ctx, dry_run=dry_run)
@task(name="python")
def clean_python(ctx, dry_run=False):
"""Cleanup python related files/dirs: *.pyc, *.pyo, ..."""
# MAYBE NOT: "**/__pycache__"
cleanup_dirs(["build", "dist", "*.egg-info", "**/__pycache__"],
dry_run=dry_run)
if not dry_run:
ctx.run("py.cleanup")
cleanup_files(["**/*.pyc", "**/*.pyo", "**/*$py.class"], dry_run=dry_run)
# -----------------------------------------------------------------------------
# TASK CONFIGURATION:
# -----------------------------------------------------------------------------
CLEANUP_EMPTY_CONFIG = {
"directories": [],
"files": [],
"extra_directories": [],
"extra_files": [],
}
def make_cleanup_config(**kwargs):
config_data = CLEANUP_EMPTY_CONFIG.copy()
config_data.update(kwargs)
return config_data
namespace = Collection(clean_all, clean_python)
namespace.add_task(clean, default=True)
namespace.configure({
"cleanup": make_cleanup_config(
files=["*.bak", "*.log", "*.tmp", "**/.DS_Store", "**/*.~*~"]
),
"cleanup_all": make_cleanup_config(
directories=[".venv*", ".tox", "downloads", "tmp"]
),
# -- BACKWARD-COMPATIBLE: OLD-STYLE
"clean": CLEANUP_EMPTY_CONFIG.copy(),
"clean_all": CLEANUP_EMPTY_CONFIG.copy(),
})
# -- EXTENSION-POINT: CLEANUP TASKS (called by: clean, clean_all task)
# NOTE: Can be used by other tasklets to register cleanup tasks.
cleanup_tasks = Collection("cleanup_tasks")
cleanup_all_tasks = Collection("cleanup_all_tasks")
# -- EXTEND NORMAL CLEANUP-TASKS:
# DISABLED: cleanup_tasks.add_task(clean_python)
#
# -----------------------------------------------------------------------------
# EXTENSION-POINT: CONFIGURATION HELPERS: Can be used from other task modules
# -----------------------------------------------------------------------------
def config_add_cleanup_dirs(directories):
# pylint: disable=protected-access
the_cleanup_directories = namespace._configuration["clean"]["directories"]
the_cleanup_directories.extend(directories)
def config_add_cleanup_files(files):
# pylint: disable=protected-access
the_cleanup_files = namespace._configuration["clean"]["files"]
the_cleanup_files.extend(files)
parse_type-0.5.6/tasks/_vendor/ 0000775 0000000 0000000 00000000000 13726616614 0016472 5 ustar 00root root 0000000 0000000 parse_type-0.5.6/tasks/_vendor/README.rst 0000664 0000000 0000000 00000002135 13726616614 0020162 0 ustar 00root root 0000000 0000000 tasks/_vendor: Bundled vendor parts -- needed by tasks
===============================================================================
This directory contains bundled archives that may be needed to run the tasks.
Especially, it contains an executable "invoke.zip" archive.
This archive can be used when invoke is not installed.
To execute invoke from the bundled ZIP archive::
python -m tasks/_vendor/invoke.zip --help
python -m tasks/_vendor/invoke.zip --version
Example for a local "bin/invoke" script in a UNIX like platform environment::
#!/bin/bash
# RUN INVOKE: From bundled ZIP file.
HERE=$(dirname $0)
python ${HERE}/../tasks/_vendor/invoke.zip $*
Example for a local "bin/invoke.cmd" script in a Windows environment::
@echo off
REM ==========================================================================
REM RUN INVOKE: From bundled ZIP file.
REM ==========================================================================
setlocal
set HERE=%~dp0
if not defined PYTHON set PYTHON=python
%PYTHON% %HERE%../tasks/_vendor/invoke.zip "%*"
parse_type-0.5.6/tasks/_vendor/invoke.zip 0000664 0000000 0000000 00000520371 13726616614 0020521 0 ustar 00root root 0000000 0000000 PK
u‹I invoke/UT =WMX^XMXux ö PK nt‹I´’b p invoke/__init__.pyUT VMXØWMXux ö uSÛnÛ0}ÏWÝÃÒÂðô-(R @7]º=ÚŠLÇZlÉÓ%—=ìÛGÚ²ãm¤•txxxH–Ö4fG´N
ªiõ
™Ò¥É²dr“|mþˆEÙÅJS×(ý$úéz3‡êRí¯°î4Gx<û „ |3ò3N