PY-51209, PY-51545: Update docstring formatting helpers to Python 3.10.

Old helpers are completely broken for reST, Google and NumPy docstring formats.

(cherry picked from commit 9ce7f986164ca7a61710eefab38934837a36f00b)

IJ-CR-16877

GitOrigin-RevId: 34f27150854279ba98afb8bb23711e8a62fa58c0
This commit is contained in:
Irina.Fediaeva
2021-12-01 10:26:49 +00:00
committed by intellij-monorepo-bot
parent 8e5c71fab9
commit b26a672d9e
16 changed files with 2086 additions and 392 deletions
+11 -21
View File
@@ -1,38 +1,28 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2016 the Pockets team, see AUTHORS.
# Copyright (c) 2018 the Pockets team, see AUTHORS.
# Licensed under the BSD License, see LICENSE for details.
"""*Let me check my pockets...*
"""
*Let me check my pockets...*
Functions available in the `pockets.*` submodules are also imported to the base
package for easy access, so::
from pockets import camel, peek_iter, resolve
from pockets import camel, iterpeek, resolve
works just as well as::
from pockets.inspect import resolve
from pockets.iterators import peek_iter
from pockets.iterators import iterpeek
from pockets.string import camel
"""
from __future__ import absolute_import
from pockets._version import __version__
from pockets.collections import is_listy, listify, mappify
from pockets.inspect import resolve
from pockets.iterators import peek_iter, modify_iter
from pockets.string import camel, uncamel, splitcaps, UnicodeMixin
from __future__ import absolute_import, print_function
import sys
from pockets.inspect import hoist_submodules
__all__ = ["__version__",
"camel",
"uncamel",
"splitcaps",
"UnicodeMixin",
"resolve",
"is_listy",
"listify",
"mappify",
"peek_iter",
"modify_iter"]
hoist_submodules(sys.modules[__name__])
+3 -1
View File
@@ -1,8 +1,10 @@
# Package versioning solution originally found here:
# http://stackoverflow.com/q/458550
__all__ = ["__version__"]
# Store the version here so:
# 1) we don't load dependencies by storing it in __init__.py
# 2) we can import it in setup.py for the same reason
# 3) we can import it into your module
__version__ = '0.3.2'
__version__ = "0.9.1"
+26
View File
@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018 the Pockets team, see AUTHORS.
# Licensed under the BSD License, see LICENSE for details.
"""
An easy to import AutoLogger instance!
>>> import logging, sys
>>> logging.basicConfig(format="%(name)s: %(message)s", stream=sys.stdout)
>>> from pockets.autolog import log
>>> log.error("Always log from the correct module.Class!") # doctest: +SKIP
pockets.autolog: Always log from the correct module.Class!
See Also:
`pockets.logger.AutoLogger`
"""
from __future__ import absolute_import, print_function
from pockets.logging import AutoLogger
__all__ = ["log"]
log = AutoLogger()
+368 -31
View File
@@ -1,32 +1,230 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2016 the Pockets team, see AUTHORS.
# Copyright (c) 2018 the Pockets team, see AUTHORS.
# Licensed under the BSD License, see LICENSE for details.
"""A pocket full of useful collection functions!"""
"""A pocket full of useful collection tools!"""
from __future__ import absolute_import
from collections import Sized, Iterable, Mapping
from __future__ import absolute_import, print_function
from collections import defaultdict
from inspect import isclass
try:
from collections.abc import Iterable, Mapping, Sized
except ImportError:
from collections import Iterable, Mapping, Sized
try:
from collections import OrderedDict
except ImportError:
OrderedDict = dict
import six
__all__ = ["is_listy", "listify", "mappify"]
__all__ = [
"groupify",
"keydefaultdict",
"is_listy",
"listify",
"is_mappy",
"mappify",
"nesteddefaultdict",
"readable_join",
"uniquify",
]
def groupify(items, keys, val_key=None):
"""
Groups a list of items into nested OrderedDicts based on the given keys.
Note:
On Python 2.6 the return value will use regular dicts instead of
OrderedDicts.
>>> from __future__ import print_function
>>> from json import dumps
>>>
>>> ex = lambda x: print(dumps(x, indent=2, sort_keys=True, default=repr))
>>>
>>> class Reminder:
... def __init__(self, when, where, what):
... self.when = when
... self.where = where
... self.what = what
... def __repr__(self):
... return 'Reminder({0.when}, {0.where}, {0.what})'.format(self)
...
>>> reminders = [
... Reminder('Fri', 'Home', 'Eat cereal'),
... Reminder('Fri', 'Work', 'Feed Ivan'),
... Reminder('Sat', 'Home', 'Sleep in'),
... Reminder('Sat', 'Home', 'Play Zelda'),
... Reminder('Sun', 'Home', 'Sleep in'),
... Reminder('Sun', 'Work', 'Reset database')]
>>>
>>> ex(groupify(reminders, 'when'))
{
"Fri": [
"Reminder(Fri, Home, Eat cereal)",
"Reminder(Fri, Work, Feed Ivan)"
],
"Sat": [
"Reminder(Sat, Home, Sleep in)",
"Reminder(Sat, Home, Play Zelda)"
],
"Sun": [
"Reminder(Sun, Home, Sleep in)",
"Reminder(Sun, Work, Reset database)"
]
}
>>>
>>> ex(groupify(reminders, ['when', 'where']))
{
"Fri": {
"Home": [
"Reminder(Fri, Home, Eat cereal)"
],
"Work": [
"Reminder(Fri, Work, Feed Ivan)"
]
},
"Sat": {
"Home": [
"Reminder(Sat, Home, Sleep in)",
"Reminder(Sat, Home, Play Zelda)"
]
},
"Sun": {
"Home": [
"Reminder(Sun, Home, Sleep in)"
],
"Work": [
"Reminder(Sun, Work, Reset database)"
]
}
}
>>>
>>> ex(groupify(reminders, ['when', 'where'], 'what'))
{
"Fri": {
"Home": [
"Eat cereal"
],
"Work": [
"Feed Ivan"
]
},
"Sat": {
"Home": [
"Sleep in",
"Play Zelda"
]
},
"Sun": {
"Home": [
"Sleep in"
],
"Work": [
"Reset database"
]
}
}
>>>
>>> ex(groupify(reminders, lambda r: '{0.when} - {0.where}'.format(r), 'what'))
{
"Fri - Home": [
"Eat cereal"
],
"Fri - Work": [
"Feed Ivan"
],
"Sat - Home": [
"Sleep in",
"Play Zelda"
],
"Sun - Home": [
"Sleep in"
],
"Sun - Work": [
"Reset database"
]
}
Args:
items (list): The list of items to arrange in groups.
keys (str|callable|list): The key or keys that should be used to group
`items`. If multiple keys are given, then each will correspond to
an additional level of nesting in the order they are given.
val_key (str|callable): A key or callable used to generate the leaf
values in the nested OrderedDicts. If `val_key` is `None`, then
the item itself is used. Defaults to `None`.
Returns:
OrderedDict: Nested OrderedDicts with `items` grouped by `keys`.
""" # noqa: E501
if not keys:
return items
keys = listify(keys)
last_key = keys[-1]
is_callable = callable(val_key)
groupified = OrderedDict()
for item in items:
current = groupified
for key in keys:
attr = key(item) if callable(key) else getattr(item, key)
if attr not in current:
current[attr] = [] if key is last_key else OrderedDict()
current = current[attr]
if val_key:
value = val_key(item) if is_callable else getattr(item, val_key)
else:
value = item
current.append(value)
return groupified
class keydefaultdict(defaultdict):
"""
A defaultdict that passes the missed key to the factory function.
>>> def echo_factory(missing_key):
... return missing_key
...
>>> d = keydefaultdict(echo_factory)
>>> d['Hello World']
'Hello World'
>>> d['Hello World'] = 'Goodbye'
>>> d['Hello World']
'Goodbye'
"""
def __missing__(self, key):
if self.default_factory is None:
raise KeyError(key)
else:
ret = self[key] = self.default_factory(key)
return ret
def is_listy(x):
"""Return True if `x` is "listy", i.e. a list-like object.
"""
Return True if `x` is "listy", i.e. a list-like object.
"Listy" is defined as a sized iterable which is neither a map nor a string:
>>> is_listy(["a", "b"])
>>> is_listy(['a', 'b'])
True
>>> is_listy(set())
True
>>> is_listy(iter(["a", "b"]))
>>> is_listy(iter(['a', 'b']))
False
>>> is_listy({"a": "b"})
>>> is_listy({'a': 'b'})
False
>>> is_listy("a regular string")
>>> is_listy('a regular string')
False
Note:
@@ -40,24 +238,30 @@ def is_listy(x):
bool: True if `x` is "listy", False otherwise.
"""
return (isinstance(x, Sized) and
isinstance(x, Iterable) and
not isinstance(x, Mapping) and
not isinstance(x, six.string_types))
return (
isinstance(x, Sized)
and isinstance(x, Iterable)
and not isinstance(x, (Mapping, type(b"")))
and not isinstance(x, six.string_types)
)
def listify(x, minlen=0, default=None, cls=None):
"""Return a listified version of `x`.
"""
Return a listified version of `x`.
If `x` is a non-string iterable, it is wrapped in a list; otherwise
a list is returned with `x` as its only element.
a list is returned with `x` as its only element. If `x` is `None`, an
empty list is returned.
>>> listify("a regular string")
>>> listify('a regular string')
['a regular string']
>>> listify(tuple(["a", "b", "c"]))
>>> listify(tuple(['a', 'b', 'c']))
['a', 'b', 'c']
>>> listify({'a': 'A'})
[{'a': 'A'}]
>>> listify(None)
[]
Note:
Not guaranteed to return a copy of `x`. If `x` is already a list and
@@ -74,15 +278,15 @@ def listify(x, minlen=0, default=None, cls=None):
[]
>>> listify([], minlen=1)
[None]
>>> listify("item", minlen=3)
>>> listify('item', minlen=3)
['item', None, None]
default (any value): Value that should be used to pad the list if it
would be shorter than `minlen`:
>>> listify([], minlen=1, default="PADDING")
>>> listify([], minlen=1, default='PADDING')
['PADDING']
>>> listify("item", minlen=3, default="PADDING")
>>> listify('item', minlen=3, default='PADDING')
['item', 'PADDING', 'PADDING']
cls (class or callable): Instead of wrapping `x` in a list, wrap it
@@ -90,7 +294,7 @@ def listify(x, minlen=0, default=None, cls=None):
as its single parameter when called:
>>> from collections import deque
>>> listify(["a", "b", "c"], cls=deque)
>>> listify(['a', 'b', 'c'], cls=deque)
deque(['a', 'b', 'c'])
Returns:
@@ -110,21 +314,56 @@ def listify(x, minlen=0, default=None, cls=None):
return x
def is_mappy(x):
"""
Return True if `x` is "mappy", i.e. a map-like object.
"Mappy" is defined as any instance of `collections.Mapping`:
>>> is_mappy({'a': 'b'})
True
>>> from collections import defaultdict
>>> is_mappy(defaultdict(list))
True
>>> is_mappy('a regular string')
False
>>> is_mappy(['a', 'b'])
False
>>> is_listy(iter({'a': 'b'}))
False
Note:
Iterables and generators fail the "mappy" test.
Args:
x (any value): The object to test.
Returns:
bool: True if `x` is "mappy", False otherwise.
"""
return isinstance(x, Mapping)
def mappify(x, default=True, cls=None):
"""Return a mappified version of `x`.
"""
Return a mappified version of `x`.
If `x` is a string, it becomes the only key of the returned dict. If `x`
is a non-string iterable, the elements of `x` become keys in the returned
dict. The values of the returned dict are set to `default`.
dict. The values of the returned dict are set to `default`. If `x` is
`None`, an empty dict is returned.
If `x` is a map, it is returned directly.
>>> mappify("a regular string")
>>> mappify('a regular string')
{'a regular string': True}
>>> mappify(["a"])
>>> mappify(['a'])
{'a': True}
>>> mappify({'a': "A"})
>>> mappify({'a': 'A'})
{'a': 'A'}
>>> mappify(None)
{}
Note:
Not guaranteed to return a copy of `x`. If `x` is already a map and
@@ -141,7 +380,7 @@ def mappify(x, default=True, cls=None):
its single parameter when called:
>>> from collections import defaultdict
>>> mappify("a", cls=lambda x: defaultdict(None, x))
>>> mappify('a', cls=lambda x: defaultdict(None, x))
defaultdict(None, {'a': True})
Returns:
@@ -151,13 +390,111 @@ def mappify(x, default=True, cls=None):
TypeError: If `x` is not a map, iterable, or string.
"""
if not isinstance(x, Mapping):
if x is None:
x = {}
elif not isinstance(x, Mapping):
if isinstance(x, six.string_types):
x = {x: default}
elif isinstance(x, Iterable):
x = dict([(v, default) for v in x])
# If cls is specified, attempt to preserve the order of x, in
# case cls is also a class that preserves order.
arg = [(v, default) for v in x]
x = OrderedDict(arg) if cls else dict(arg)
else:
raise TypeError("Unable to mappify {0}".format(type(x)), x)
raise TypeError(
"Unable to mappify non-mappy {0}".format(type(x)), x
)
if cls and not (isclass(cls) and issubclass(type(x), cls)):
x = cls(x)
return x
def nesteddefaultdict():
"""
A defaultdict that returns nested defaultdicts as the default value.
Each defaultdict returned as the default value will also return nested
defaultdicts, and so on.
>>> nested = nesteddefaultdict()
>>> nested_child = nested['New Key 1']
>>> nested_child
defaultdict(...)
>>> nested_grandchild = nested_child['New Key 2']
>>> nested_grandchild
defaultdict(...)
"""
return defaultdict(nesteddefaultdict)
def readable_join(xs, conjunction="and", sep=","):
"""
Accepts a list of strings and separates them with commas as grammatically
appropriate with a conjunction before the final entry. Any input strings
containing only whitespace will not be included in the result.
>>> readable_join(['foo'])
'foo'
>>> readable_join(['foo', 'bar'])
'foo and bar'
>>> readable_join(['foo', 'bar', 'baz'])
'foo, bar, and baz'
>>> readable_join(['foo', ' ', '', 'bar', '', ' ', 'baz'])
'foo, bar, and baz'
>>> readable_join(['foo', 'bar', 'baz'], 'or')
'foo, bar, or baz'
>>> readable_join(['foo', 'bar', 'baz'], 'but never')
'foo, bar, but never baz'
"""
xs = [s for s in map(lambda s: str(s).strip(), listify(xs)) if s]
if len(xs) > 1:
xs = list(xs)
xs[-1] = conjunction + " " + xs[-1]
return (sep + " " if len(xs) > 2 else " ").join(xs)
def uniquify(x, key=lambda o: o, cls=None):
"""
Returns an order-preserved copy of `x` with duplicate items removed.
>>> uniquify(['a', 'z', 'a', 'b', 'a', 'y', 'a', 'c', 'a', 'x'])
['a', 'z', 'b', 'y', 'c', 'x']
Args:
x (Sequence): Sequence to uniquify.
key (str or callable): Similar to `sorted`, specifies an attribute or
function of one argument that is used to extract a comparison key
from each list element: key=str.lower. By default, compares the
elements directly.
>>> strings = ['ASDF', 'asdf', 'ZXCV', 'zxcv']
>>> uniquify(strings, key=str.lower)
['ASDF', 'ZXCV']
cls (class or callable): Instead of wrapping `x` in a list, wrap it
in an instance of `cls`. `cls` should accept an iterable object
as its single parameter when called:
>>> from collections import deque
>>> listify(['a', 'b', 'c'], cls=deque)
deque(['a', 'b', 'c'])
Returns:
list: An order-preserved copy of `x` with duplicate items removed.
Raises:
TypeError: If `x` is not "listy".
"""
if not is_listy(x):
raise TypeError("Unable to uniquify non-listy {0}".format(type(x)), x)
seen = set()
keys = [(key(o) if callable(key) else getattr(o, key), o) for o in x]
x = [o for k, o in keys if k not in seen and not seen.add(k)]
if cls and not (isclass(cls) and issubclass(type(x), cls)):
x = cls(x)
+129
View File
@@ -0,0 +1,129 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018 the Pockets team, see AUTHORS.
# Licensed under the BSD License, see LICENSE for details.
"""A pocket full of useful datetime tools!"""
from __future__ import absolute_import, print_function
import sys
from datetime import datetime, timedelta
__all__ = [
"ceil_datetime",
"floor_datetime",
"round_datetime",
"timedelta_total_seconds",
]
if sys.version_info < (2, 7):
def timedelta_total_seconds(td):
"""
Python 2.6 replacement function for timedelta.total_seconds().
Args:
td (datetime.timedelta): A `datetime.timedelta` instance.
Returns:
float: The total number of seconds plus the fractional
number of microseconds in `td`.
"""
total_seconds = td.seconds + (td.days * 24.0 * 3600.0)
return (td.microseconds + (total_seconds * 1.0e6)) / 1.0e6
else:
def timedelta_total_seconds(td):
"""
Python 2.6 replacement function for timedelta.total_seconds().
Args:
td (datetime.timedelta): A `datetime.timedelta` instance.
Returns:
float: The total number of seconds plus the fractional
number of microseconds in `td`.
"""
return td.total_seconds()
def ceil_datetime(dt, nearest):
"""
Rounds the given `datetime` up to the nearest `timedelta` increment.
Note:
`dt.microsecond` is always set to zero and ignored.
Args:
dt (datetime.datetime): The `datetime` instance to ceil.
nearest (datetime.timedelta): The `timedelta` to use as the increment.
Returns:
datetime.datetime: A copy of `dt` ceiled to `nearest`.
"""
dt = dt.replace(microsecond=0)
dt_min = datetime.min.replace(tzinfo=dt.tzinfo)
secs = timedelta_total_seconds(dt_min - dt)
nearest_secs = timedelta_total_seconds(nearest)
total_delta_secs = secs % nearest_secs
delta_days = total_delta_secs // 86400 # (60 * 60 * 24)
delta_secs = total_delta_secs % 86400.0 # (60 * 60 * 24)
return dt + timedelta(days=delta_days, seconds=delta_secs)
def floor_datetime(dt, nearest):
"""
Rounds the given `datetime` down to the nearest `timedelta` increment.
Note:
`dt.microsecond` is always set to zero and ignored.
Args:
dt (datetime.datetime): The `datetime` instance to floor.
nearest (datetime.timedelta): The `timedelta` to use as the increment.
Returns:
datetime.datetime: A copy of `dt` floored to `nearest`.
"""
dt = dt.replace(microsecond=0)
dt_min = datetime.min.replace(tzinfo=dt.tzinfo)
secs = timedelta_total_seconds(dt - dt_min)
nearest_secs = timedelta_total_seconds(nearest)
total_delta_secs = secs % nearest_secs
delta_days = total_delta_secs // 86400 # (60 * 60 * 24)
delta_secs = total_delta_secs % 86400.0 # (60 * 60 * 24)
return dt - timedelta(days=delta_days, seconds=delta_secs)
def round_datetime(dt, nearest):
"""
Rounds the given `datetime` up/down to the nearest `timedelta` increment.
Note:
`dt.microsecond` is always set to zero and ignored.
Args:
dt (datetime.datetime): The `datetime` instance to round.
nearest (datetime.timedelta): The `timedelta` to use as the increment.
Returns:
datetime.datetime: A copy of `dt` rounded to `nearest`.
"""
dt = dt.replace(microsecond=0)
dt_min = datetime.min.replace(tzinfo=dt.tzinfo)
secs = timedelta_total_seconds(dt - dt_min)
nearest_secs = timedelta_total_seconds(nearest)
rounded_secs = ((secs + (nearest_secs / 2)) // nearest_secs) * nearest_secs
total_delta_secs = rounded_secs - secs
delta_days = total_delta_secs // 86400 # (60 * 60 * 24)
delta_secs = total_delta_secs % 86400.0 # (60 * 60 * 24)
return dt + timedelta(days=delta_days, seconds=delta_secs)
+171
View File
@@ -0,0 +1,171 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018 the Pockets team, see AUTHORS.
# Licensed under the BSD License, see LICENSE for details.
"""A pocket full of useful decorators!"""
from __future__ import absolute_import, print_function
import inspect
from functools import wraps
from pockets.collections import listify
from pockets.inspect import unwrap
__all__ = [
"argmod",
"cached_classproperty",
"cached_property",
"classproperty",
]
def argmod(*args):
"""
Decorator that intercepts and modifies function arguments.
Args:
from_param (str|list): A parameter or list of possible parameters that
should be modified using `modifier_func`. Passing a list of
possible parameters is useful when a function's parameter names
have changed, but you still want to support the old parameter
names.
to_param (str): Optional. If given, to_param will be used as the
parameter name for the modified argument. If not given, to_param
will default to the last parameter given in `from_param`.
modifier_func (callable): The function used to modify the `from_param`.
Returns:
function: A function that modifies the given `from_param` before the
function is called.
"""
from_param = listify(args[0])
to_param = from_param[-1] if len(args) < 3 else args[1]
modifier_func = args[-1]
def _decorator(func):
try:
argspec = inspect.getfullargspec(unwrap(func))
except AttributeError:
argspec = inspect.getargspec(unwrap(func))
if to_param not in argspec.args:
return func
arg_index = argspec.args.index(to_param)
@wraps(func)
def _modifier(*args, **kwargs):
kwarg = False
for arg in from_param:
if arg in kwargs:
kwarg = arg
break
if kwarg:
kwargs[to_param] = modifier_func(kwargs.pop(kwarg))
elif arg_index < len(args):
args = list(args)
args[arg_index] = modifier_func(args[arg_index])
return func(*args, **kwargs)
return _modifier
return _decorator
class cached_classproperty(property):
"""
Like @cached_property except it works on classes instead of instances.
Note:
Class properties created by @cached_classproperty are read-only.
Any attempts to write to the property will erase the
@cached_classproperty, and the behavior of the underlying method
will be lost.
>>> class MyClass(object):
... @cached_classproperty
... def myproperty(cls):
... return '{0}.myproperty'.format(cls.__name__)
>>> MyClass.myproperty
'MyClass.myproperty'
"""
def __init__(self, fget, *arg, **kw):
super(cached_classproperty, self).__init__(fget, *arg, **kw)
self.__doc__ = fget.__doc__
self.__fget_name__ = fget.__name__
def __get__(desc, self, cls):
cache_attr = "_cached_{0}_{1}".format(cls.__name__, desc.__fget_name__)
if not hasattr(cls, cache_attr):
setattr(cls, cache_attr, desc.fget(cls))
return getattr(cls, cache_attr)
def getter(self, fget):
raise AttributeError("@cached_classproperty.getter is not supported")
def setter(self, fset):
raise AttributeError("@cached_classproperty.setter is not supported")
def deleter(self, fdel):
raise AttributeError("@cached_classproperty.deleter is not supported")
def cached_property(func):
"""Decorator for making readonly, memoized properties."""
cache_attr = "_cached_{0}".format(func.__name__)
@property
@wraps(func)
def caching(self, *args, **kwargs):
if not hasattr(self, cache_attr):
setattr(self, cache_attr, func(self, *args, **kwargs))
return getattr(self, cache_attr)
return caching
class classproperty(property):
"""
Decorator to create a read-only class property similar to classmethod.
For whatever reason, the @property decorator isn't smart enough to
recognize @classmethods and behaves differently on them than on instance
methods. This decorator may be used like to create a class-level property,
useful for singletons and other one-per-class properties.
This implementation is partially based on
`sqlalchemy.util.langhelpers.classproperty`.
Note:
Class properties created by @classproperty are read-only. Any attempts
to write to the property will erase the @classproperty, and the
behavior of the underlying method will be lost.
>>> class MyClass(object):
... @classproperty
... def myproperty(cls):
... return '{0}.myproperty'.format(cls.__name__)
>>> MyClass.myproperty
'MyClass.myproperty'
"""
def __init__(self, fget, *arg, **kw):
super(classproperty, self).__init__(fget, *arg, **kw)
self.__doc__ = fget.__doc__
def __get__(desc, self, cls):
return desc.fget(cls)
def getter(self, fget):
raise AttributeError("@classproperty.getter is not supported")
def setter(self, fset):
raise AttributeError("@classproperty.setter is not supported")
def deleter(self, fdel):
raise AttributeError("@classproperty.deleter is not supported")
+315 -24
View File
@@ -1,21 +1,245 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2016 the Pockets team, see AUTHORS.
# Copyright (c) 2018 the Pockets team, see AUTHORS.
# Licensed under the BSD License, see LICENSE for details.
"""A pocket full of useful reflection functions!"""
from __future__ import absolute_import
from __future__ import absolute_import, print_function
import inspect
import functools
from os.path import basename
from pkgutil import iter_modules
from pockets.collections import listify
import six
from six import string_types
__all__ = ["resolve"]
from pockets.collections import listify
from pockets.string import splitify
__all__ = [
"collect_subclasses",
"collect_superclasses",
"collect_superclass_attr_names",
"hoist_submodules",
"import_star",
"import_submodules",
"is_data",
"resolve",
"unwrap",
]
def collect_subclasses(cls):
"""
Recursively collects all descendant subclasses that inherit from the
given class, not including the class itself.
Note:
Does not include `cls` itself.
Args:
cls (class): The class object from which the collection should begin.
Returns:
list: A list of `class` objects that inherit from `cls`. This list
will not include `cls` itself.
"""
subclasses = set()
for subclass in cls.__subclasses__():
subclasses.add(subclass)
subclasses.update(collect_subclasses(subclass))
return list(subclasses)
def collect_superclasses(cls, terminal_class=None, modules=None):
"""
Recursively collects all ancestor superclasses in the inheritance
hierarchy of the given class, including the class itself.
Note:
Inlcudes `cls` itself. Will not include `terminal_class`.
Args:
cls (class): The class object from which the collection should begin.
terminal_class (class or list): If `terminal_class` is encountered in
the hierarchy, we stop ascending the tree. `terminal_class` will
not be included in the returned list.
modules (string, module, or list): If `modules` is passed, we only
return classes that are in the given module/modules. This can be
used to exclude base classes that come from external libraries.
Returns:
list: A list of `class` objects from which `cls` inherits. This list
will include `cls` itself.
"""
terminal_class = listify(terminal_class)
if modules is not None:
modules = listify(modules)
module_strings = []
for m in modules:
if isinstance(m, six.string_types):
module_strings.append(m)
else:
module_strings.append(m.__name__)
modules = module_strings
superclasses = set()
is_in_module = modules is None or cls.__module__ in modules
if is_in_module and cls not in terminal_class:
superclasses.add(cls)
for base in cls.__bases__:
superclasses.update(
collect_superclasses(base, terminal_class, modules)
)
return list(superclasses)
def collect_superclass_attr_names(cls, terminal_class=None, modules=None):
"""
Recursively collects all attribute names of ancestor superclasses in the
inheritance hierarchy of the given class, including the class itself.
Note:
Inlcudes `cls` itself. Will not include `terminal_class`.
Args:
cls (class): The class object from which the collection should begin.
terminal_class (class or list): If `terminal_class` is encountered in
the hierarchy, we stop ascending the tree. Attributes from
`terminal_class` will not be included in the returned list.
modules (string, module, or list): If `modules` is passed, we only
return classes that are in the given module/modules. This can be
used to exclude base classes that come from external libraries.
Returns:
list: A list of `str` attribute names for every `class` in the
inheritance hierarchy.
"""
superclasses = collect_superclasses(cls, terminal_class, modules)
attr_names = set()
for superclass in superclasses:
attr_names.update(superclass.__dict__.keys())
return list(attr_names)
def hoist_submodules(package, extend_all=True):
"""
Sets `__all__` attrs from submodules of `package` as attrs on `package`.
Note:
This only considers attributes exported by `__all__`. If a submodule
does not define `__all__`, then it is ignored.
Effectively does::
from package.* import *
Args:
package (str or module): The parent package into which submodule
exports should be hoisted.
extend_all (bool): If True, `package.__all__` will be extended
to include the hoisted attributes. Defaults to True.
Returns:
list: List of all hoisted attribute names.
"""
module = resolve(package)
hoisted_attrs = []
for submodule in import_submodules(module):
for attr_name, attr in import_star(submodule).items():
hoisted_attrs.append(attr_name)
setattr(module, attr_name, attr)
if extend_all:
if getattr(module, "__all__", None) is None:
module.__all__ = list(hoisted_attrs)
else:
module.__all__.extend(hoisted_attrs)
return hoisted_attrs
def import_star(module):
"""
Imports all exported attributes of `module` and returns them in a `dict`.
Note:
This only considers attributes exported by `__all__`. If `module`
does not define `__all__`, then nothing is imported.
Effectively does::
from module import *
Args:
module (str or module): The module from which a wildcard import
should be done.
Returns:
dict: Map of all imported attributes.
"""
module = resolve(module)
attrs = getattr(module, "__all__", [])
return dict([(attr, getattr(module, attr)) for attr in attrs])
def import_submodules(package):
"""
Imports all submodules of `package`.
Effectively does::
__import__(package.*)
Args:
package (str or module): The parent package from which submodules
should be imported.
Yields:
module: The next submodule of `package`.
"""
module = resolve(package)
if basename(module.__file__).startswith("__init__.py"):
for _, submodule_name, _ in iter_modules(module.__path__):
yield resolve(submodule_name, module)
def is_data(obj):
"""
Returns True if `obj` is a "data like" object.
Strongly inspired by `inspect.classify_class_attrs`. This function is
useful when trying to determine if an attribute has a meaningful docstring
or not. In general, a routine can have meaningful docstrings, whereas
non-routines cannot.
See Also:
* `inspect.classify_class_attrs`
* `inspect.isroutine`
Args:
obj (object): The object in question.
Returns:
bool: True if `obj` is "data like", False otherwise.
"""
if isinstance(
obj, (staticmethod, classmethod, property)
) or inspect.isroutine(obj):
return False
else:
return True
def resolve(name, modules=None):
"""Resolve a dotted name to an object (usually class, module, or function).
"""
Resolve a dotted name to an object (usually class, module, or function).
If `name` is a string, attempt to resolve it according to Python
dot notation, e.g. "path.to.MyClass". If `name` is anything other than a
@@ -23,7 +247,7 @@ def resolve(name, modules=None):
>>> resolve("calendar.TextCalendar")
<class 'calendar.TextCalendar'>
>>> resolve(object()) #doctest: +ELLIPSIS
>>> resolve(object())
<object object at 0x...>
If `modules` is specified, then resolution of `name` is restricted
@@ -37,6 +261,8 @@ def resolve(name, modules=None):
no leading dots, resolution is first attempted absolutely and then
relative to the calling module.
Pass an empty string for `modules` to only use absolute resolution.
Warning:
Do not resolve strings supplied by an end user without specifying
`modules`. Instantiating an arbitrary object specified by an end user
@@ -47,18 +273,18 @@ def resolve(name, modules=None):
Restricting `name` resolution to a set of `modules`:
>>> resolve("pockets.camel") #doctest: +ELLIPSIS
>>> resolve("pockets.camel")
<function camel at 0x...>
>>> resolve("pockets.camel", modules=["re", "six"]) #doctest: +ELLIPSIS
>>> resolve("pockets.camel", modules=["re", "six"])
Traceback (most recent call last):
...
ValueError: Unable to resolve 'pockets.camel' in modules: ['re', 'six']
...
Args:
name (str or object): A dotted name.
modules (str or list, optional): A module or list of modules, under
which to search for `name`.
modules (str, module, or list, optional): A module or list of modules,
under which to search for `name`.
Returns:
object: The object specified by `name`.
@@ -70,16 +296,16 @@ def resolve(name, modules=None):
if not isinstance(name, string_types):
return name
obj_path = name.split('.')
obj_path = splitify(name, ".", include_empty=True)
search_paths = []
if modules:
while not obj_path[0]:
if modules is not None:
while not obj_path[0].strip():
obj_path.pop(0)
for module_path in listify(modules):
search_paths.append(module_path.split('.') + obj_path)
search_paths.append(splitify(module_path, ".") + obj_path)
else:
caller = inspect.getouterframes(inspect.currentframe())[1][0].f_globals
module_path = caller['__name__'].split('.')
module_path = caller["__name__"].split(".")
if not obj_path[0]:
obj_path.pop(0)
while not obj_path[0]:
@@ -93,13 +319,78 @@ def resolve(name, modules=None):
search_paths.append(obj_path)
search_paths.append(module_path + obj_path)
exceptions = []
for path in search_paths:
try:
obj = functools.reduce(getattr, path[1:], __import__(path[0]))
except (AttributeError, ImportError):
pass
else:
return obj
# Import the most deeply nested module available
module = None
module_path = []
obj_path = list(path)
while obj_path:
module_name = obj_path.pop(0)
while not module_name:
module_name = obj_path.pop(0)
if isinstance(module_name, string_types):
package = ".".join(module_path + [module_name])
try:
module = __import__(package, fromlist=module_name)
except ImportError as ex:
exceptions.append(ex)
obj_path = [module_name] + obj_path
break
else:
module_path.append(module_name)
else:
module = module_name
module_path.append(module.__name__)
raise ValueError("Unable to resolve '{0}' "
"in modules: {1}".format(name, modules))
if module:
if obj_path:
try:
return functools.reduce(getattr, obj_path, module)
except AttributeError as ex:
exceptions.append(ex)
else:
return module
if modules:
msg = "Unable to resolve '{0}' in modules: {1}".format(name, modules)
else:
msg = "Unable to resolve '{0}'".format(name)
if exceptions:
msgs = ["{0}: {1}".format(type(e).__name__, e) for e in exceptions]
raise ValueError("\n ".join([msg] + msgs))
else:
raise ValueError(msg)
def unwrap(func):
"""
Finds the innermost function that has been wrapped using `functools.wrap`.
Note:
This function relies on the existence of the `__wrapped__` attribute,
which was not automatically added until Python 3.2. If you are using
an older version of Python, you'll have to manually add the
`__wrapped__` attribute in order to use `unwrap`::
def my_decorator(func):
@wraps(func)
def with_my_decorator(*args, **kwargs):
return func(*args, **kwargs)
if not hasattr(with_my_decorator, '__wrapped__'):
with_my_decorator.__wrapped__ = func
return with_my_decorator
Args:
func (function): A function that may or may not have been wrapped
using `functools.wrap`.
Returns:
function: The original function before it was wrapped using
`functools.wrap`. `func` is returned directly, if it was never
wrapped using `functools.wrap`.
"""
return unwrap(func.__wrapped__) if hasattr(func, "__wrapped__") else func
+44 -27
View File
@@ -1,21 +1,24 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2016 the Pockets team, see AUTHORS.
# Copyright (c) 2018 the Pockets team, see AUTHORS.
# Licensed under the BSD License, see LICENSE for details.
"""A pocket full of useful iterators!"""
from __future__ import absolute_import
from __future__ import absolute_import, print_function
import collections
import six
__all__ = ["peek_iter", "modify_iter"]
__all__ = ["itermod", "iterpeek", "modify_iter", "peek_iter"]
class peek_iter(object):
"""An iterator object that supports peeking ahead.
class iterpeek(object):
"""
An iterator object that supports peeking ahead.
>>> p = peek_iter(["a", "b", "c", "d", "e"])
>>> p = iterpeek(["a", "b", "c", "d", "e"])
>>> p.peek()
'a'
>>> p.next()
@@ -39,17 +42,18 @@ class peek_iter(object):
raised, otherwise the value will be returned.
See Also:
`peek_iter` can operate as a drop in replacement for the built-in
`iter <http://docs.python.org/2/library/functions.html#iter>`_
`iterpeek` can operate as a drop in replacement for the built-in
`iter <https://docs.python.org/3/library/functions.html#iter>`_
function.
Attributes:
sentinel (any value): The value used to indicate the iterator is
exhausted. If `sentinel` was not given when the `peek_iter` was
exhausted. If `sentinel` was not given when the `iterpeek` was
instantiated, then it will be set to a new object
instance: ``object()``.
"""
def __init__(self, *args):
"""__init__(o, sentinel=None)"""
self._iterable = iter(*args)
@@ -62,7 +66,7 @@ class peek_iter(object):
def __next__(self, n=None):
# NOTE: Prevent 2to3 from transforming self.next() in next(self),
# which causes an infinite loop!
return getattr(self, 'next')(n)
return getattr(self, "next")(n)
def _fillcache(self, n):
"""Cache `n` items. If `n` is 0 or None, then 1 item is cached."""
@@ -76,7 +80,8 @@ class peek_iter(object):
self._cache.append(self.sentinel)
def has_next(self):
"""Determine if iterator is exhausted.
"""
Determine if iterator is exhausted.
Returns:
bool: True if iterator has more items, False otherwise.
@@ -88,7 +93,8 @@ class peek_iter(object):
return self.peek() != self.sentinel
def next(self, n=None):
"""Get the next item or `n` items of the iterator.
"""
Get the next item or `n` items of the iterator.
Args:
n (int, optional): The number of items to retrieve. Defaults to
@@ -100,7 +106,7 @@ class peek_iter(object):
the items will be returned in a list. If `n` is 0, an empty
list is returned:
>>> p = peek_iter(["a", "b", "c", "d", "e"])
>>> p = iterpeek(["a", "b", "c", "d", "e"])
>>> p.next()
'a'
>>> p.next(0)
@@ -130,7 +136,8 @@ class peek_iter(object):
return result
def peek(self, n=None):
"""Preview the next item or `n` items of the iterator.
"""
Preview the next item or `n` items of the iterator.
The iterator is not advanced when peek is called.
@@ -144,10 +151,10 @@ class peek_iter(object):
the items will be returned in a list. If `n` is 0, an empty
list is returned.
If the iterator is exhausted, `peek_iter.sentinel` is returned,
If the iterator is exhausted, `iterpeek.sentinel` is returned,
or placed as the last item in the returned list:
>>> p = peek_iter(["a", "b", "c"])
>>> p = iterpeek(["a", "b", "c"])
>>> p.sentinel = "END"
>>> p.peek()
'a'
@@ -172,8 +179,13 @@ class peek_iter(object):
return result
class modify_iter(peek_iter):
"""An iterator object that supports modifying items as they are returned.
# Backwards compatibility
peek_iter = iterpeek
class itermod(iterpeek):
"""
An iterator object that supports modifying items as they are returned.
>>> a = [" A list ",
... " of strings ",
@@ -181,7 +193,7 @@ class modify_iter(peek_iter):
... " extra ",
... " whitespace. "]
>>> modifier = lambda s: s.strip().replace('with', 'without')
>>> for s in modify_iter(a, modifier=modifier):
>>> for s in itermod(a, modifier=modifier):
... print('"%s"' % s)
"A list"
"of strings"
@@ -218,29 +230,30 @@ class modify_iter(peek_iter):
the item.
Values returned by `peek` as well as `next` are affected by
`modifier`. However, `modify_iter.sentinel` is never passed through
`modifier`. However, `itermod.sentinel` is never passed through
`modifier`; it will always be returned from `peek` unmodified.
"""
def __init__(self, *args, **kwargs):
"""__init__(o, sentinel=None, modifier=lambda x: x)"""
if 'modifier' in kwargs:
self.modifier = kwargs['modifier']
if "modifier" in kwargs:
self.modifier = kwargs["modifier"]
elif len(args) > 2:
self.modifier = args[2]
args = args[:2]
else:
self.modifier = lambda x: x
if not six.callable(self.modifier):
raise TypeError('modify_iter(o, modifier): '
'modifier must be callable')
super(modify_iter, self).__init__(*args)
raise TypeError("itermod(o, modifier): modifier must be callable")
super(itermod, self).__init__(*args)
def _fillcache(self, n):
"""Cache `n` modified items. If `n` is 0 or None, 1 item is cached.
"""
Cache `n` modified items. If `n` is 0 or None, 1 item is cached.
Each item returned by the iterator is passed through the
`modify_iter.modified` function before being cached.
`itermod.modified` function before being cached.
"""
if not n:
@@ -251,3 +264,7 @@ class modify_iter(peek_iter):
except StopIteration:
while len(self._cache) < n:
self._cache.append(self.sentinel)
# Backwards compatibility
modify_iter = itermod
+322
View File
@@ -0,0 +1,322 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2018 the Pockets team, see AUTHORS.
# Licensed under the BSD License, see LICENSE for details.
"""
A pocket full of useful logging tools!
The `pockets.logging` module adds a `logging.TRACE` level and a
`logging.Logger.trace` method, so messages can be logged at a lower priority
level than `logging.DEBUG`.
"""
from __future__ import absolute_import, print_function
import inspect
import logging
import logging.config
import sys
from functools import wraps
from six import text_type as unicode_type
__all__ = [
"log_exceptions",
"AutoLogger",
"EagerFormattingAdapter",
"IndentMultilineLogFormatter",
]
TRACE = 5
logging.addLevelName(TRACE, "TRACE")
logging.TRACE = TRACE
def trace(self, message, *args, **kwargs):
# Yes, _log() takes '*args' as 'args'.
self._log(TRACE, message, args, **kwargs)
logging.Logger.trace = trace
def log_exceptions(fn):
"""
Decorator that wraps a function and logs any raised exceptions.
The exception will still be raised after being logged. Also logs the
arguments to every call at the trace level.
"""
from pockets.autolog import log
@wraps(fn)
def wrapper(*args, **kwargs):
try:
a = [str(x)[:255] for x in args]
kw = dict([(k[:255], str(v)[:255]) for k, v in kwargs.items()])
log.trace("Calling %s.%s %r %r", fn.__module__, fn.__name__, a, kw)
return fn(*args, **kwargs)
except Exception as e:
log.error("Error calling function %s: %s" % (fn.__name__, e))
log.exception(e)
raise
return wrapper
class AutoLogger(object):
"""
A logger proxy object with all of the methods and attributes of `Logger`.
When an attribute (e.g., "debug") is requested, inspects the stack for the
calling module's name, and passes that name to `logging.getLogger`.
What this means is that you can instantiate an `AutoLogger` anywhere, and
when you call it, the log entry shows the module where you called it, not
where it was created.
`AutoLogger` also inspects the local variables where it is called, looking
for `self`. If `self` exists, its classname is added to the module name.
Args:
adapter_class (LoggerAdapter): optional `LoggerAdapter` class to use.
adapter_args (list): optional args to use when instantiating an
instance of `adapter_class`.
adapter_kwargs (dict): optional kwargs to use when instantiating an
instance of `adapter_class`.
"""
def __init__(
self, adapter_class=None, adapter_args=None, adapter_kwargs=None
):
if adapter_args is None:
adapter_args = []
if adapter_kwargs is None:
adapter_kwargs = {}
self.adapter_class = adapter_class
self.adapter_args = adapter_args
self.adapter_kwargs = adapter_kwargs
def __getattr__(self, name):
f_locals = inspect.currentframe().f_back.f_locals
if "self" in f_locals and f_locals["self"] is not None:
other = f_locals["self"]
caller_name = "%s.%s" % (
other.__class__.__module__,
other.__class__.__name__,
)
else:
caller_name = inspect.currentframe().f_back.f_globals["__name__"]
logger = logging.getLogger(caller_name)
if self.adapter_class:
logger = self.adapter_class(
logger, *self.adapter_args, **self.adapter_kwargs
)
return getattr(logger, name)
class EagerFormattingAdapter(logging.LoggerAdapter):
"""
A `LoggerAdapter` that immediately interpolates message arguments if the
appropriate loglevel is set.
This is useful because many log handlers generate log output on a separate
thread, and the value of the log arguments may have changed by the time
the handler interpolates them. This can lead to confusion when debugging
difficult bugs, as the log output will not reflect what was actually
happening when the log message was originally generated.
For performance reasons, the interpolation ONLY happens if the appropriate
loglevel is set. This prevents unnecessary string formatting on log
messages that will just be thrown out anyway.
Args:
logger (Logger): The underlying Logger instance to use.
extra (dict): Extra args, ignored by this implementation.
"""
def __init__(self, logger, extra=None):
"""
Initialize the adapter with a logger and a dict-like object which
provides contextual information. This constructor signature allows
easy stacking of LoggerAdapters, if so desired.
You can effectively pass keyword arguments as shown in the
following example::
adapter = LoggerAdapter(someLogger, dict(p1=v1, p2="v2"))
"""
self.logger = logger
self.extra = extra
def _eagerFormat(self, msg, level, args):
"""
Eagerly apply log formatting if the appropriate level is enabled.
Otherwise we just drop the log message (and return a string indicating
that it was suppreseed).
"""
if not hasattr(self, "isEnabledFor") or self.isEnabledFor(level):
# Do the string formatting immediately.
if args:
return self._getUnterpolatedMessage(msg, args)
else:
return msg
else:
# Otherwise, just drop the message completely to avoid anything
# going wrong in the future. This text shoudl clue one in to
# what's going on in the bizarre edge case where this ever does
# show up.
return "(log message suppressed due to insufficient log level)"
def _getUnterpolatedMessage(self, msg, args):
"""
Returns the formatted string, will first attempt str.format and will
fallback to msg % args as it was originally.
This is lifted almost wholesale from logging_unterpolation.
"""
original_msg = msg
try:
msg = msg.format(*args)
except UnicodeEncodeError:
# This is most likely due to formatting a non-ascii string argument
# into a bytestring, which the %-operator automatically handles
# by casting the left side (the "msg" variable) in this context
# to unicode. So we'll do that here
#
# Handle the attempt to print utf-8 encoded data, similar to
# %-interpolation's handling of unicode formatting non-ascii
# strings
msg = unicode_type(msg).format(*args)
except ValueError:
# From PEP-3101, value errors are of the type raised by the format
# method itself, so see if we should fall back to original
# formatting since there was an issue
if "%" in msg:
msg = msg % args
else:
# We should NOT fall back, since there's no possible string
# interpolation happening and we want a meaningful error
# message
raise
if msg == original_msg and "%" in msg:
# There must have been no string formatting methods used, given
# the presence of args without a change in the msg
if len(args) == 1 and isinstance(args[0], dict):
# Handles cases like:
# logging.debug("a %(a)d b %(b)s", {'a':1, 'b':2})
msg = msg % args[0]
else:
# Fall back to original formatting
msg = msg % args
return msg
def trace(self, msg, *args, **kwargs):
"""
Delegate a trace call to the underlying logger, after adding
contextual information from this adapter instance.
"""
self.log(logging.TRACE, msg, *args, **kwargs)
def debug(self, msg, *args, **kwargs):
"""
Delegate a debug call to the underlying logger, after adding
contextual information from this adapter instance.
"""
self.log(logging.DEBUG, msg, *args, **kwargs)
def info(self, msg, *args, **kwargs):
"""
Delegate an info call to the underlying logger, after adding
contextual information from this adapter instance.
"""
self.log(logging.INFO, msg, *args, **kwargs)
def warn(self, msg, *args, **kwargs):
"""
Delegate a warning call to the underlying logger, after adding
contextual information from this adapter instance.
"""
self.log(logging.WARN, msg, *args, **kwargs)
def warning(self, msg, *args, **kwargs):
"""
Delegate a warning call to the underlying logger, after adding
contextual information from this adapter instance.
"""
self.log(logging.WARNING, msg, *args, **kwargs)
def error(self, msg, *args, **kwargs):
"""
Delegate an error call to the underlying logger, after adding
contextual information from this adapter instance.
"""
self.log(logging.ERROR, msg, *args, **kwargs)
def exception(self, msg, *args, **kwargs):
"""
Delegate an exception call to the underlying logger, after adding
contextual information from this adapter instance.
"""
kwargs["exc_info"] = 1
self.log(logging.ERROR, msg, *args, **kwargs)
def critical(self, msg, *args, **kwargs):
"""
Delegate a critical call to the underlying logger, after adding
contextual information from this adapter instance.
"""
self.log(logging.CRITICAL, msg, *args, **kwargs)
def fatal(self, msg, *args, **kwargs):
"""
Delegate a fatal call to the underlying logger, after adding
contextual information from this adapter instance.
"""
self.log(logging.FATAL, msg, *args, **kwargs)
def log(self, level, msg, *args, **kwargs):
"""
Delegate a log call to the underlying logger, after adding
contextual information from this adapter instance.
"""
msg, kwargs = self.process(msg, kwargs)
# We explicitly do not pass the args into the log method here, since
# they should be "used up" by the eagerFormat method.
self.logger.log(level, self._eagerFormat(msg, level, args), **kwargs)
class IndentMultilineLogFormatter(logging.Formatter):
"""
Formatter which indents messages that are split across multiple lines.
Indents all lines that start with a newline so they are easier for
external log programs to parse.
"""
def format(self, record):
"""
Formats the given `LogRecord` by indenting all newlines.
Args:
record (LogRecord): The `LogRecord` to format.
Returns:
str: The formatted message with all newlines indented.
"""
if sys.version_info < (2, 7):
s = logging.Formatter.format(self, record)
else:
s = super(IndentMultilineLogFormatter, self).format(record)
return s.rstrip("\n").replace("\n", "\n ")
+192 -49
View File
@@ -1,54 +1,72 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2016 the Pockets team, see AUTHORS.
# Copyright (c) 2018 the Pockets team, see AUTHORS.
# Licensed under the BSD License, see LICENSE for details.
"""A pocket full of useful string manipulation functions!"""
"""A pocket full of useful string manipulation tools!"""
from __future__ import absolute_import, print_function
from __future__ import absolute_import
import re
import six
import sys
from pockets.collections import listify
from pockets.collections import is_listy, listify
__all__ = [
"camel",
"uncamel",
"fieldify",
"unfieldify",
"sluggify",
"splitcaps",
"splitify",
"UnicodeMixin",
]
__all__ = ["camel", "uncamel", "splitcaps"]
# Default regular expression flags
if six.PY2:
_re_flags = re.L | re.M | re.U
RE_FLAGS = re.L | re.M | re.U
else:
_re_flags = re.M | re.U
RE_FLAGS = re.M | re.U
_whitespace_group_re = re.compile("(\s+)", _re_flags)
RE_NONWORD = re.compile(r"[\W_]+")
_uncamel_re = re.compile(
"(" # The whole expression is in a single group
RE_SPLITCAPS = re.compile(
# Clause 1
"(?<=[^\sA-Z])" # Preceded by neither a space nor a capital letter
"[A-Z]+[^a-z\s]*" # All non-lowercase beginning with a capital letter
"(?=[A-Z][^A-Z\s]*?[a-z]|\s|$)" # Followed by a capitalized word
"|"
r"[A-Z]+[^a-z]*" # All non-lowercase beginning with a capital letter
r"(?=[A-Z][^A-Z]*?[a-z]|$)" # Followed by a capitalized word
r"|"
# Clause 2
"(?<=[^\s])" # Preceded by a character that is not a space
"[A-Z][^A-Z\s]*?[a-z]+[^A-Z\s]*" # Capitalized word
")", _re_flags)
_splitcaps_re = re.compile(
# Clause 1
"[A-Z]+[^a-z]*" # All non-lowercase beginning with a capital letter
"(?=[A-Z][^A-Z]*?[a-z]|$)" # Followed by a capitalized word
"|"
# Clause 2
"[A-Z][^A-Z]*?[a-z]+[^A-Z]*" # Capitalized word
"|"
r"[A-Z][^A-Z]*?[a-z]+[^A-Z]*" r"|" # Capitalized word
# Clause 3
"[^A-Z]+", # All non-uppercase
_re_flags)
r"[^A-Z]+", # All non-uppercase
RE_FLAGS,
)
RE_UNCAMEL = re.compile(
r"(" # The whole expression is in a single group
# Clause 1
r"(?<=[^\sA-Z])" # Preceded by neither a space nor a capital letter
r"[A-Z]+[^a-z\s]*" # All non-lowercase beginning with a capital letter
r"(?=[A-Z][^A-Z\s]*?[a-z]|\s|$)" # Followed by a capitalized word
r"|"
# Clause 2
r"(?<=[^\s])" # Preceded by a character that is not a space
r"[A-Z][^A-Z\s]*?[a-z]+[^A-Z\s]*" # Capitalized word
r")",
RE_FLAGS,
)
RE_WHITESPACE_GROUP = re.compile(r"(\s+)", RE_FLAGS)
def camel(s, sep="_", lower_initial=False, upper_segments=None,
preserve_upper=False):
"""Convert underscore_separated string (aka snake_case) to CamelCase.
def camel(
s, sep="_", lower_initial=False, upper_segments=None, preserve_upper=False
):
"""
Convert underscore_separated string (aka snake_case) to CamelCase.
Works on full sentences as well as individual words:
@@ -123,7 +141,7 @@ def camel(s, sep="_", lower_initial=False, upper_segments=None,
lower_initial = listify(lower_initial)
upper_segments = listify(upper_segments)
result = []
for word in _whitespace_group_re.split(s):
for word in RE_WHITESPACE_GROUP.split(s):
segments = [segment for segment in word.split(sep) if segment]
count = len(segments)
for i, segment in enumerate(segments):
@@ -149,7 +167,8 @@ def camel(s, sep="_", lower_initial=False, upper_segments=None,
def uncamel(s, sep="_"):
"""Convert CamelCase string to underscore_separated (aka snake_case).
"""
Convert CamelCase string to underscore_separated (aka snake_case).
A CamelCase word is considered to be any uppercase letter followed by zero
or more lowercase letters. Contiguous groups of uppercase letters – like
@@ -184,11 +203,88 @@ def uncamel(s, sep="_"):
str: uncamel_cased version of `s`.
"""
return _uncamel_re.sub(r'{0}\1'.format(sep), s).lower()
return RE_UNCAMEL.sub(r"{0}\1".format(sep), s).lower()
def fieldify(s, sep="_"):
"""
Convert a string into a valid "field-like" variable name.
Converts `s` from camel case to underscores, and replaces all spaces and
non-word characters with `sep`:
>>> fieldify('The XmlHTTPRequest Contained, "DATA..."')
'the_xml_http_request_contained_data'
Args:
s (str): The string to fieldify.
sep (str): The string to use as a word separator in the returned field.
Defaults to '_'.
Returns:
str: The field version of `s`.
"""
if not s:
return ""
return RE_NONWORD.sub(sep, uncamel(s)).strip(sep)
def unfieldify(s, sep="_"):
"""
Makes a best effort to reverse the algorithm from `fieldify`.
Replaces instances of `sep` in `s` with a space and converts the result to
title case:
>>> unfieldify('the_xml_http_request_contained_data')
'The Xml Http Request Contained Data'
Args:
s (str): The string to fieldify.
sep (str): The string to consider a word separator in `s`.
Defaults to '_'.
Returns:
str: The unfieldified version of `s`.
"""
if not s:
return ""
s = s.strip(r"{0} ".format(sep))
return (" ".join([w for w in s.split(sep) if w])).title()
def sluggify(s, sep="-"):
"""
Convert a string into a "slug" suitable for use in a URL.
Converts `s` to lower case, and replaces all spaces and non-word
characters with `sep`:
>>> sluggify('The ANGRY Wizard Shouted, "HEY..."')
'the-angry-wizard-shouted-hey'
Args:
s (str): The string to convert into a slug.
sep (str): The string to use as a word separator in the slug.
Defaults to '-'.
Returns:
str: The sluggify version of `s`.
"""
if not s:
return ""
return RE_NONWORD.sub(sep, s).lower().strip(sep)
def splitcaps(s, pattern=None, maxsplit=None, flags=0):
"""Intelligently split a string on capitalized words.
"""
Intelligently split a string on capitalized words.
A capitalized word is considered to be any uppercase letter followed by
zero or more lowercase letters. Contiguous groups of uppercase letters –
@@ -215,13 +311,13 @@ def splitcaps(s, pattern=None, maxsplit=None, flags=0):
['lower case words']
Does not split on whitespace by default. To also split
on whitespace, pass "\\\s+" for `pattern`:
on whitespace, pass "\\\\s+" for `pattern`:
>>> splitcaps("Without whiteSpace pattern")
['Without white', 'Space pattern']
>>> splitcaps("With whiteSpace pattern", pattern="\s+")
>>> splitcaps("With whiteSpace pattern", pattern=r"\\s+")
['With', 'white', 'Space', 'pattern']
>>> splitcaps("With whiteSpace group", pattern="(\s+)")
>>> splitcaps("With whiteSpace group", pattern=r"(\\s+)")
['With', ' ', 'white', 'Space', ' ', 'group']
Args:
@@ -254,13 +350,13 @@ def splitcaps(s, pattern=None, maxsplit=None, flags=0):
maxsplit = -1
if pattern:
pattern_re = re.compile(pattern, flags or _re_flags)
pattern_re = re.compile(pattern, flags or RE_FLAGS)
else:
pattern_re = None
result = []
post_maxsplit = []
for m in _splitcaps_re.finditer(s):
for m in RE_SPLITCAPS.finditer(s):
if pattern_re:
for segment in pattern_re.split(m.group()):
if segment:
@@ -273,8 +369,8 @@ def splitcaps(s, pattern=None, maxsplit=None, flags=0):
if maxsplit > 0 and len(result) >= maxsplit:
if m.end() < len(s):
post_maxsplit.append(s[m.end():])
post_maxsplit = ''.join(post_maxsplit)
post_maxsplit.append(s[m.end() :])
post_maxsplit = "".join(post_maxsplit)
if post_maxsplit:
result.append(post_maxsplit)
break
@@ -282,9 +378,53 @@ def splitcaps(s, pattern=None, maxsplit=None, flags=0):
return result if len(result) > 0 else [s]
def splitify(value, separator=",", strip=True, include_empty=False):
"""
Convert a value to a list using a supercharged `split()`.
If `value` is a string, it is split by `separator`. If `separator` is
`None` or empty, no attempt to split is made, and `value` is returned as
the only item in a list.
If `strip` is `True`, then the split strings will be stripped of
whitespace. If `strip` is a string, then the split strings will be
stripped of the given string.
If `include_empty` is `False`, then empty split strings will not be
included in the returned list.
If `value` is `None` an empty list is returned.
If `value` is already "listy", it is returned as-is.
If `value` is any other type, it is returned as the only item in a list.
>>> splitify("first item, second item")
['first item', 'second item']
>>> splitify("first path: second path: :skipped empty path", ":")
['first path', 'second path', 'skipped empty path']
>>> splitify(["already", "split"])
['already', 'split']
>>> splitify(None)
[]
>>> splitify(1969)
[1969]
"""
if is_listy(value):
return value
if isinstance(value, str) and separator:
parts = value.split(separator)
if strip:
strip = None if strip is True else strip
parts = [s.strip(strip) for s in parts]
return [s for s in parts if include_empty or s]
return listify(value)
class UnicodeMixin(object):
"""Mixin class to define the proper __str__/__unicode__ methods in
Python 2 or 3.
"""
Mixin class to define proper __str__/__unicode__ methods in Python 2 or 3.
Originally found on the `Porting Python 2 Code to Python 3 HOWTO`_.
@@ -293,9 +433,12 @@ class UnicodeMixin(object):
"""
if sys.version_info[0] >= 3: # Python 3
if six.PY2:
def __str__(self):
return self.__unicode__().encode("utf8")
else:
def __str__(self):
return self.__unicode__()
else: # Python 2
def __str__(self):
return self.__unicode__().encode('utf8')
+1 -1
View File
@@ -6,7 +6,7 @@
This package is a namespace package that contains all extensions
distributed in the ``sphinx-contrib`` distribution.
:copyright: Copyright 2007-2014 by the Sphinx team, see AUTHORS.
:copyright: Copyright 2007-2018 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
+151 -63
View File
@@ -1,57 +1,79 @@
# -*- coding: utf-8 -*-
# Copyright 2014 Rob Ruana
# Licensed under the BSD License, see LICENSE file for details.
"""
sphinxcontrib.napoleon
~~~~~~~~~~~~~~~~~~~~~~
"""Sphinx napoleon extension -- support for NumPy and Google style docstrings.
Support for NumPy and Google style docstrings.
:copyright: Copyright 2013-2018 by Rob Ruana, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
import sys
from six import iteritems
from sphinxcontrib.napoleon.docstring import GoogleDocstring, NumpyDocstring
from sphinxcontrib.napoleon._version import __version__
assert __version__ # silence pyflakes
from sphinxcontrib.napoleon.docstring import GoogleDocstring, NumpyDocstring
if False:
# For type annotation
from typing import Any, Dict, List # NOQA
class Config(object):
class Config:
"""Sphinx napoleon extension settings in `conf.py`.
Listed below are all the settings used by napoleon and their default
values. These settings can be changed in the Sphinx `conf.py` file. Make
sure that both "sphinx.ext.autodoc" and "sphinxcontrib.napoleon" are
enabled in `conf.py`::
sure that "sphinxcontrib.napoleon" is enabled in `conf.py`::
# conf.py
# Add any Sphinx extension module names here, as strings
extensions = ['sphinx.ext.autodoc', 'sphinxcontrib.napoleon']
extensions = ['sphinxcontrib.napoleon']
# Napoleon settings
napoleon_google_docstring = True
napoleon_numpy_docstring = True
napoleon_include_init_with_doc = False
napoleon_include_private_with_doc = False
napoleon_include_special_with_doc = True
napoleon_include_special_with_doc = False
napoleon_use_admonition_for_examples = False
napoleon_use_admonition_for_notes = False
napoleon_use_admonition_for_references = False
napoleon_use_ivar = False
napoleon_use_param = True
napoleon_use_rtype = True
napoleon_use_keyword = True
napoleon_custom_sections = None
.. _Google style:
http://google.github.io/styleguide/pyguide.html
https://google.github.io/styleguide/pyguide.html
.. _NumPy style:
https://github.com/numpy/numpy/blob/master/doc/HOWTO_DOCUMENT.rst.txt
Attributes
----------
napoleon_google_docstring : bool, defaults to True
napoleon_google_docstring : :obj:`bool` (Defaults to True)
True to parse `Google style`_ docstrings. False to disable support
for Google style docstrings.
napoleon_numpy_docstring : bool, defaults to True
napoleon_numpy_docstring : :obj:`bool` (Defaults to True)
True to parse `NumPy style`_ docstrings. False to disable support
for NumPy style docstrings.
napoleon_include_private_with_doc : bool, defaults to False
napoleon_include_init_with_doc : :obj:`bool` (Defaults to False)
True to list ``__init___`` docstrings separately from the class
docstring. False to fall back to Sphinx's default behavior, which
considers the ``__init___`` docstring as part of the class
documentation.
**If True**::
def __init__(self):
\"\"\"
This will be included in the docs because it has a docstring
\"\"\"
def __init__(self):
# This will NOT be included in the docs
napoleon_include_private_with_doc : :obj:`bool` (Defaults to False)
True to include private members (like ``_membername``) with docstrings
in the documentation. False to fall back to Sphinx's default behavior.
@@ -67,7 +89,7 @@ class Config(object):
# This will NOT be included in the docs
pass
napoleon_include_special_with_doc : bool, defaults to True
napoleon_include_special_with_doc : :obj:`bool` (Defaults to False)
True to include special members (like ``__membername__``) with
docstrings in the documentation. False to fall back to Sphinx's
default behavior.
@@ -84,7 +106,7 @@ class Config(object):
# This will NOT be included in the docs
return unicode(self.__class__.__name__)
napoleon_use_admonition_for_examples : bool, defaults to False
napoleon_use_admonition_for_examples : :obj:`bool` (Defaults to False)
True to use the ``.. admonition::`` directive for the **Example** and
**Examples** sections. False to use the ``.. rubric::`` directive
instead. One may look better than the other depending on what HTML
@@ -108,7 +130,7 @@ class Config(object):
This is just a quick example
napoleon_use_admonition_for_notes : bool, defaults to False
napoleon_use_admonition_for_notes : :obj:`bool` (Defaults to False)
True to use the ``.. admonition::`` directive for **Notes** sections.
False to use the ``.. rubric::`` directive instead.
@@ -121,7 +143,7 @@ class Config(object):
--------
:attr:`napoleon_use_admonition_for_examples`
napoleon_use_admonition_for_references : bool, defaults to False
napoleon_use_admonition_for_references : :obj:`bool` (Defaults to False)
True to use the ``.. admonition::`` directive for **References**
sections. False to use the ``.. rubric::`` directive instead.
@@ -129,7 +151,7 @@ class Config(object):
--------
:attr:`napoleon_use_admonition_for_examples`
napoleon_use_ivar : bool, defaults to False
napoleon_use_ivar : :obj:`bool` (Defaults to False)
True to use the ``:ivar:`` role for instance variables. False to use
the ``.. attribute::`` directive instead.
@@ -149,11 +171,11 @@ class Config(object):
.. attribute:: attr1
*int*
Description of `attr1`
napoleon_use_param : bool, defaults to True
:type: int
napoleon_use_param : :obj:`bool` (Defaults to True)
True to use a ``:param:`` role for each function parameter. False to
use a single ``:parameters:`` role for all the parameters.
@@ -180,7 +202,22 @@ class Config(object):
* **arg2** (*int, optional*) --
Description of `arg2`, defaults to 0
napoleon_use_rtype : bool, defaults to True
napoleon_use_keyword : :obj:`bool` (Defaults to True)
True to use a ``:keyword:`` role for each function keyword argument.
False to use a single ``:keyword arguments:`` role for all the
keywords.
This behaves similarly to :attr:`napoleon_use_param`. Note unlike
docutils, ``:keyword:`` and ``:param:`` will not be treated the same
way - there will be a separate "Keyword Arguments" section, rendered
in the same fashion as "Parameters" section (type links created if
possible)
See Also
--------
:attr:`napoleon_use_param`
napoleon_use_rtype : :obj:`bool` (Defaults to True)
True to use the ``:rtype:`` role for the return type. False to output
the return type inline with the description.
@@ -200,28 +237,46 @@ class Config(object):
:returns: *bool* -- True if successful, False otherwise
napoleon_custom_sections : :obj:`list` (Defaults to None)
Add a list of custom sections to include, expanding the list of parsed sections.
The entries can either be strings or tuples, depending on the intention:
* To create a custom "generic" section, just pass a string.
* To create an alias for an existing section, pass a tuple containing the
alias name and the original, in that order.
If an entry is just a string, it is interpreted as a header for a generic
section. If the entry is a tuple/list/indexed container, the first entry
is the name of the section, the second is the section key to emulate.
"""
_config_values = {
'napoleon_google_docstring': (True, 'env'),
'napoleon_numpy_docstring': (True, 'env'),
'napoleon_include_init_with_doc': (False, 'env'),
'napoleon_include_private_with_doc': (False, 'env'),
'napoleon_include_special_with_doc': (True, 'env'),
'napoleon_include_special_with_doc': (False, 'env'),
'napoleon_use_admonition_for_examples': (False, 'env'),
'napoleon_use_admonition_for_notes': (False, 'env'),
'napoleon_use_admonition_for_references': (False, 'env'),
'napoleon_use_ivar': (False, 'env'),
'napoleon_use_param': (True, 'env'),
'napoleon_use_rtype': (True, 'env'),
'napoleon_use_keyword': (True, 'env'),
'napoleon_custom_sections': (None, 'env')
}
def __init__(self, **settings):
for name, (default, rebuild) in iteritems(self._config_values):
# type: (Any) -> None
for name, (default, rebuild) in self._config_values.items():
setattr(self, name, default)
for name, value in iteritems(settings):
for name, value in settings.items():
setattr(self, name, value)
def setup(app):
# type: (Sphinx) -> Dict[unicode, Any]
"""Sphinx extension setup function.
When the extension is loaded, Sphinx imports this module and executes
@@ -242,20 +297,45 @@ def setup(app):
`The Extension API <http://sphinx-doc.org/extdev/appapi.html>`_
"""
from sphinx.application import Sphinx
if not isinstance(app, Sphinx):
return # probably called by tests
# probably called by tests
return {'version': __version__, 'parallel_read_safe': True}
_patch_python_domain()
app.setup_extension('sphinx.ext.autodoc')
app.connect('autodoc-process-docstring', _process_docstring)
app.connect('autodoc-skip-member', _skip_member)
for name, (default, rebuild) in iteritems(Config._config_values):
for name, (default, rebuild) in Config._config_values.items():
app.add_config_value(name, default, rebuild)
return {'version': __version__, 'parallel_read_safe': True}
def _patch_python_domain():
# type: () -> None
try:
from sphinx.domains.python import PyTypedField
except ImportError:
pass
else:
import sphinx.domains.python
from sphinx.locale import _
for doc_field in sphinx.domains.python.PyObject.doc_field_types:
if doc_field.name == 'parameter':
doc_field.names = ('param', 'parameter', 'arg', 'argument')
break
sphinx.domains.python.PyObject.doc_field_types.append(
PyTypedField('keyword', label=_('Keyword Arguments'),
names=('keyword', 'kwarg', 'kwparam'),
typerolename='obj', typenames=('paramtype', 'kwtype'),
can_collapse=True))
def _process_docstring(app, what, name, obj, options, lines):
# type: (Sphinx, unicode, unicode, Any, Any, List[unicode]) -> None
"""Process the docstring for a given python object.
Called when autodoc has read and processed a docstring. `lines` is a list
@@ -292,6 +372,7 @@ def _process_docstring(app, what, name, obj, options, lines):
"""
result_lines = lines
docstring = None # type: GoogleDocstring
if app.config.napoleon_numpy_docstring:
docstring = NumpyDocstring(result_lines, app.config, app, what, name,
obj, options)
@@ -304,11 +385,14 @@ def _process_docstring(app, what, name, obj, options, lines):
def _skip_member(app, what, name, obj, skip, options):
# type: (Sphinx, unicode, unicode, Any, bool, Any) -> bool
"""Determine if private and special class members are included in docs.
The following settings in conf.py determine if private and special class
members are included in the generated documentation:
members or init methods are included in the generated documentation:
* ``napoleon_include_init_with_doc`` --
include init methods if they have docstrings
* ``napoleon_include_private_with_doc`` --
include private members if they have docstrings
* ``napoleon_include_special_with_doc`` --
@@ -345,43 +429,47 @@ def _skip_member(app, what, name, obj, skip, options):
"""
has_doc = getattr(obj, '__doc__', False)
is_member = (what == 'class' or what == 'exception' or what == 'module')
if name != '__weakref__' and name != '__init__' and has_doc and is_member:
if name != '__weakref__' and has_doc and is_member:
cls_is_owner = False
if what == 'class' or what == 'exception':
if sys.version_info[0] < 3:
cls = getattr(obj, 'im_class', getattr(obj, '__objclass__',
None))
cls_is_owner = (cls and hasattr(cls, name) and
name in cls.__dict__)
elif sys.version_info[1] >= 3:
qualname = getattr(obj, '__qualname__', '')
cls_path, _, _ = qualname.rpartition('.')
if cls_path:
try:
if '.' in cls_path:
import importlib
import functools
import six
if six.PY2 and (what == 'class' or what == 'exception'):
cls = getattr(obj, 'im_class', getattr(obj, '__objclass__',
None))
cls_is_owner = (cls and hasattr(cls, name) and
name in cls.__dict__)
elif what == 'class' or what == 'exception':
qualname = getattr(obj, '__qualname__', '')
cls_path, _, _ = qualname.rpartition('.')
if cls_path:
try:
if '.' in cls_path:
import importlib
import functools
mod = importlib.import_module(obj.__module__)
mod_path = cls_path.split('.')
cls = functools.reduce(getattr, mod_path, mod)
else:
cls = obj.__globals__[cls_path]
except:
cls_is_owner = False
mod = importlib.import_module(obj.__module__)
mod_path = cls_path.split('.')
cls = functools.reduce(getattr, mod_path, mod)
else:
cls_is_owner = (cls and hasattr(cls, name) and
name in cls.__dict__)
else:
cls = obj.__globals__[cls_path]
except Exception:
cls_is_owner = False
else:
cls_is_owner = (cls and hasattr(cls, name) and # type: ignore
name in cls.__dict__)
else:
cls_is_owner = True
cls_is_owner = False
if what == 'module' or cls_is_owner:
is_special = name.startswith('__') and name.endswith('__')
is_private = not is_special and name.startswith('_')
is_init = (name == '__init__')
is_special = (not is_init and name.startswith('__') and
name.endswith('__'))
is_private = (not is_init and not is_special and
name.startswith('_'))
inc_init = app.config.napoleon_include_init_with_doc
inc_special = app.config.napoleon_include_special_with_doc
inc_private = app.config.napoleon_include_private_with_doc
if (is_special and inc_special) or (is_private and inc_private):
if ((is_special and inc_special) or
(is_private and inc_private) or
(is_init and inc_init)):
return False
return skip
return None
@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
"""
sphinxcontrib.napoleon._upstream
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Functions to help compatibility with upstream sphinx.ext.napoleon.
:copyright: Copyright 2013-2018 by Rob Ruana, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
# Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
def _(message, *args):
"""
NOOP implementation of sphinx.locale.get_translation shortcut.
"""
return message
@@ -5,4 +5,4 @@
# 1) we don't load dependencies by storing it in __init__.py
# 2) we can import it in setup.py for the same reason
# 3) we can import it into your module
__version__ = '0.3.11'
__version__ = '0.7'
File diff suppressed because it is too large Load Diff
@@ -148,7 +148,7 @@ public abstract class SectionBasedDocString extends DocStringLineParser implemen
/**
* Used to parse e.g. optional function signature at the beginning of NumPy-style docstring
*
* @return first line from which to star parsing remaining sections
* @return first line from which to start parsing remaining sections
*/
protected int parseHeader(int startLine) {
return startLine;