Skip to content

Commit

Permalink
CLEANUP: SYNC w/ original parse. Remove any extensions (on this branch).
Browse files Browse the repository at this point in the history
  • Loading branch information
jenisys committed Mar 18, 2018
1 parent 7914aca commit f65d124
Show file tree
Hide file tree
Showing 11 changed files with 30 additions and 1,480 deletions.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
.idea/
.tox/
__pycache__/
downloads/
.coverage
.pytest_cache
parse.egg-info
Expand Down
30 changes: 20 additions & 10 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ 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:
The module is set up to only export ``parse()``, ``search()`` and
``findall()`` when ``import *`` is used:

>>> from parse import *

Expand Down Expand Up @@ -91,6 +91,9 @@ spam
>>> 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
--------------------
Expand Down Expand Up @@ -287,19 +290,26 @@ A more complete example of a custom type might be:
... return yesno_mapping[text.lower()]


----
Potential Gotchas
-----------------

`parse()` will always match the shortest text necessary (from left to right)
to fulfil the parse pattern, so for example:

**Unreleased Changes**:
>>> pattern = '{dir1}/{dir2}'
>>> data = 'root/parent/subdir'
>>> parse(pattern, data).named
{'dir1': 'root', 'dir2': 'parent/subdir'}

- Add optional cardinality field support after type field in parse expressions.
- Add Cardinality, TypeBuilder classes to support different cardinality.
- Add parse_type module to simplify type creation for common use cases.
- Add with_pattern() decorator for type-converter functions.
- Add support for optional 'pattern' attribute in user-defined types.
So, even though `{'dir1': 'root/parent', 'dir2': 'subdir'}` would also fit
the pattern, the actual match represents the shortest successful match for
`dir1`.

----

**Version history (in brief)**:

- 1.8.2 clarify message on invalid format specs (thanks Rick Teachey)
- 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
Expand Down
244 changes: 4 additions & 240 deletions parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,14 +289,6 @@
----
**Unreleased Changes**:
- Add optional cardinality field support after type field in parse expressions.
- Add Cardinality, TypeBuilder classes to support different cardinality.
- Add parse_type module to simplify type creation for common use cases.
- Add with_pattern() decorator for type-converter functions.
- Add support for optional 'pattern' attribute in user-defined types.
**Version history (in brief)**:
- 1.8.2 clarify message on invalid format specs (thanks Rick Teachey)
Expand Down Expand Up @@ -580,200 +572,7 @@ class RepeatedNameError(ValueError):
ALLOWED_TYPES = set(list('nbox%fegwWdDsS') +
['t' + c for c in 'ieahgcts'])

# -----------------------------------------------------------------------------
# CLASS: Cardinality (Field Part)
# -----------------------------------------------------------------------------
class Cardinality(object):
"""
Cardinality field 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..
"""
one = 1
zero_or_one = 2
zero_or_more = 3
one_or_more = 4

# -- ALIASES:
optional = zero_or_one
many = one_or_more

# -- MAPPING SUPPORT:
pattern_chars = "?*+"
from_char_map = {
'?': zero_or_one,
'*': zero_or_more,
'+': one_or_more,
}

@classmethod
def make_zero_or_one_pattern(cls, pattern):
return r"(%s)?" % pattern

@classmethod
def make_zero_or_more_pattern(cls, pattern, listsep=','):
return r"(%s)?(\s*%s\s*(%s))*" % (pattern, listsep, pattern)

@classmethod
def make_one_or_more_pattern(cls, pattern, listsep=','):
return r"(%s)(\s*%s\s*(%s))*" % (pattern, listsep, pattern)

# -- OPTIONAL CODE:
# @classmethod
# def make_pattern_for(cls, cardinality, pattern, listsep=','):
# """
# Creates a new regular expression pattern for the cardinality.
#
# :param cardinality: Cardinality case (zero_or_one, zero_or_more, ...)
# :param pattern: Regex pattern for cardinality one (as text).
# :param listsep: Optional list separator for many (default: comma ',').
# :return: New regular expression pattern for this cardinality case.
# """
# if cardinality == cls.zero_or_one:
# return cls.make_zero_or_one_pattern(pattern)
# elif cardinality == cls.zero_or_more:
# return cls.make_zero_or_more_pattern(pattern, listsep)
# elif cardinality == cls.one_or_more:
# return cls.make_one_or_more_pattern(pattern, listsep)
# # -- OTHERWISE, EXPECT: Cardinality one, otherwise OOPS.
# assert cls.is_one(cardinality), "Unknown value: %s" % cardinality
# return pattern
#
# @classmethod
# def is_one(cls, cardinality):
# return cardinality == cls.one or cardinality is None
#
# @classmethod
# def is_many(cls, cardinality):
# return cardinality == cls.one_or_more or cardinality == cls.zero_or_more
#
# -----------------------------------------------------------------------------
# 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.
"""
default_pattern = r".+?"

@classmethod
def with_zero_or_one(cls, parse_type):
"""
Creates a type-converter function for a T with 0..1 times
by using the type-converter for one item of T.
:param parse_type: Type-converter (function) for data type T.
:return: type-converter for optional<T> (T or None).
"""
def parse_optional(text, m=None):
if text:
text = text.strip()
if not text:
return None
return parse_type(text)
pattern = getattr(parse_type, "pattern", cls.default_pattern)
new_pattern = Cardinality.make_zero_or_one_pattern(pattern)
parse_optional.pattern = new_pattern
return parse_optional

@classmethod
def with_zero_or_more(cls, parse_type, listsep=",", max_size=None):
"""
Creates a type-converter function for a list<T> with 0..N items
by using the type-converter for one item of T.
:param parse_type: Type-converter (function) for data type T.
:param listsep: Optional list separator between items (default: ',')
:param max_size: Optional max. number of items constraint (future).
:return: type-converter for list<T>
"""
def parse_list0(text, m=None):
if text:
text = text.strip()
if not text:
return []
parts = [ parse_type(texti.strip())
for texti in text.split(listsep) ]
return parts
pattern = getattr(parse_type, "pattern", cls.default_pattern)
list_pattern = Cardinality.make_zero_or_more_pattern(pattern, listsep)
parse_list0.pattern = list_pattern
parse_list0.max_size = max_size
return parse_list0

@classmethod
def with_one_or_more(cls, parse_type, listsep=",", max_size=None):
"""
Creates a type-converter function for a list<T> with 1..N items
by using the type-converter for one item of T.
:param parse_type: Type-converter (function) for data type T.
:param listsep: Optional list separator between items (default: ',')
:param max_size: Optional max. number of items constraint (future).
:return: type-converter for list<T>
"""
def parse_list(text, m=None):
parts = [ parse_type(texti.strip())
for texti in text.split(listsep) ]
return parts
pattern = getattr(parse_type, "pattern", cls.default_pattern)
list_pattern = Cardinality.make_one_or_more_pattern(pattern, listsep)
parse_list.pattern = list_pattern
parse_list.max_size = max_size
return parse_list

# -- ALIAS METHODS:
@classmethod
def with_optional(cls, parse_type):
"""Alias for :py:meth:`with_zero_or_one` method."""
return cls.with_zero_or_one(parse_type)

@classmethod
def with_many(cls, parse_type, **kwargs):
"""Alias for :py:meth:`with_one_or_more` method."""
return cls.with_one_or_more(parse_type, **kwargs)

@classmethod
def with_many0(cls, parse_type, **kwargs):
"""Alias for :py:meth:`with_zero_or_more` method."""
return cls.with_zero_or_more(parse_type, **kwargs)

# -----------------------------------------------------------------------------
# DECORATOR: with_pattern
# -----------------------------------------------------------------------------
def with_pattern(pattern):
"""
Provides a decorator for type-converter (parse_type) functions.
Annotates the type converter with the :attr:`pattern` attribute.

EXAMPLE:
>>> import parse
>>> @parse.with_pattern(r"\d+")
... def parse_number(text):
... return int(text)
is equivalent to:
>>> def parse_number(text):
... return int(text)
>>> parse_number.pattern = r"\d+"
:param pattern: Regular expression pattern (as text).
:return: Wrapped function
"""
def decorator(func):
func.pattern = pattern
return func
return decorator


# -----------------------------------------------------------------------------
# FUNCTIONS: Parse Helpers
# -----------------------------------------------------------------------------
def extract_format(format, extra_types):
'''Pull apart the format [[fill]align][0][width][.precision][type]
'''
Expand All @@ -797,14 +596,6 @@ def extract_format(format, extra_types):
break
width += format[0]
format = format[1:]
# -- CARDINALITY-FIELD:
cardinality = None
if format and format[-1] in Cardinality.pattern_chars:
_cardinality_char = format[-1]
cardinality = Cardinality.from_char_map[_cardinality_char]
format = format[:-1]
assert format, "Type information is required for cardinality"
# -- CARDINALITY-FIELD END.

if format.startswith('.'):
# Precision isn't needed but we need to capture it so that
Expand Down Expand Up @@ -1081,38 +872,16 @@ def _handle_field(self, field):

# decode the format specification
format = extract_format(format, self._extra_types)
cardinality = format["cardinality"]

# figure type conversions, if any
type = format['type']
is_numeric = type and type in 'n%fegdobh'
if type in self._extra_types:
type_converter = self._extra_types[type]
# -- EXTENSION: cardinality-field
s = getattr(type_converter, 'pattern', TypeBuilder.default_pattern)
if cardinality == Cardinality.one_or_more:
# -- CASE MANY one_or_more: list<T> as comma-separated list.
f = TypeBuilder.with_one_or_more(type_converter)
s = f.pattern
elif cardinality == Cardinality.zero_or_more:
# -- CASE MANY zero_or_more: list<T> as comma-separated list.
f = TypeBuilder.with_zero_or_more(type_converter)
s = f.pattern
elif cardinality == Cardinality.zero_or_one:
# -- CASE zero_or_one: optional<T> := T or None
f = TypeBuilder.with_zero_or_one(type_converter)
# -- NOT HERE: s = f.pattern
# OPTIONAL case is better handled below.
else:
# -- CASE one: T
def f(string, m):
return type_converter(string)
# -- DISABLED ORIGINAL PART:
# s = getattr(type_converter, 'pattern', r'.+?')
#
# def f(string, m):
# return type_converter(string)
# -- EXTENSION-END: cardinality-field
s = getattr(type_converter, 'pattern', r'.+?')

def f(string, m):
return type_converter(string)
self._type_conversions[group] = f
elif type == 'n':
s = '\d{1,3}([,.]\d{3})*'
Expand Down Expand Up @@ -1230,11 +999,6 @@ def f(string, m):
if not fill:
fill = ' '

if cardinality == Cardinality.zero_or_one:
# -- CARDINALITY: Make field optional.
assert wrap, "Cardinality requires wrap"
wrap += '?'

# 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:
Expand Down
Loading

0 comments on commit f65d124

Please sign in to comment.