Coverage for pandalone.xlref._xlref : 100%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
#!/usr/bin/env python # -*- coding: UTF-8 -*- # # Copyright 2014 European Commission (JRC); # Licensed under the EUPL (the 'Licence'); # You may not use this work except in compliance with the Licence. # You may obtain a copy of the Licence at: http://ec.europa.eu/idabc/eupl The user-facing implementation of *xlref*.
Prefer accessing the public members from the parent module. """
# TODO: Try different backends providing `colname` function. except ImportError: log.warning( 'One of `xlrd`, `...` libraries is needed, will crash later!')
"""When `True`, most coord-functions accept any 2-tuples."""
"""The key for specifying options within :term:`filters`."""
""" A pair of 1-based strings, denoting the "A1" coordinates of a cell.
The "num" coords (numeric, 0-based) are specified using numpy-arrays (:class:`Coords`). """
""" A pair of 0-based integers denoting the "num" coordinates of a cell.
The "A1" coords (1-based coordinates) are specified using :class:`Cell`. """ # return np.array([row, cell], dtype=np.int16)
"""Make *A1* :class:`Cell` from *resolved* coords, with rudimentary error-checking.
Examples::
>>> coords2Cell(row=0, col=0) Cell(row='1', col='A') >>> coords2Cell(row=0, col=26) Cell(row='1', col='AA')
>>> coords2Cell(row=10, col='.') Cell(row='11', col='.')
>>> coords2Cell(row=-3, col=-2) Traceback (most recent call last): AssertionError: negative row!
"""
""" All the infos required to :term:`target` a cell.
:param Cell land: the :term:`landing-cell` :param str mov: use None for missing moves. :param str mod: one of (`+`, `-` or `None`)
An :term:`Edge` might be "cooked" or "uncooked" depending on its `land`:
- An *uncooked* edge contains *A1* :class:`Cell`. - An *cooked* edge contains a *resolved* :class:`Coords`.
"""
"""Make optional the last 2 fields of :class:`Edge` ``(mov, mod)`` ."""
""" Make a new `Edge` from any non-values supplied, as is capitalized, or nothing.
:param str, None col: ie ``A`` :param str, None row: ie ``1`` :param str, None mov: ie ``RU`` :param str, None mod: ie ``+``
:return: a `Edge` if any non-None :rtype: Edge, None
Examples::
>>> tr = Edge_uncooked('1', 'a', 'Rul', '-') >>> tr Edge(land=Cell(row='1', col='A'), mov='RUL', mod='-')
No error checking performed::
>>> Edge_uncooked('Any', 'foo', 'BaR', '+_&%') Edge(land=Cell(row='Any', col='FOO'), mov='BAR', mod='+_&%')
>>> print(Edge_uncooked(None, None, None, None)) None
except were coincidental::
>>> Edge_uncooked(row=0, col=123, mov='BAR', mod=None) Traceback (most recent call last): AttributeError: 'int' object has no attribute 'upper'
>>> Edge_uncooked(row=0, col='A', mov=123, mod=None) Traceback (most recent call last): AttributeError: 'int' object has no attribute 'upper' """
mov=mov and mov.upper(), mod=mod)
'L': Coords(0, -1), 'U': Coords(-1, 0), 'R': Coords(0, 1), 'D': Coords(1, 0) }
r""" ^\s*(?:(?P<sh_name>[^!]+)?!)? # xl sheet name (?: # 1st-edge (?: (?: (?P<st_col>[A-Z]+|[_^]) # A1-col (?P<st_row>[123456789]\d*|[_^]) # A1-row ) | (?: R(?P<st_row2>-?[123456789]\d*|[_^.]) # RC-row C(?P<st_col2>-?[123456789]\d*|[_^.]) # RC-col ) ) (?:\( (?P<st_mov>L|U|R|D|LD|LU|UL|UR|RU|RD|DL|DR) # moves (?P<st_mod>[+-])? # move modifiers \) )? )? (?:(?P<colon>:) # ':' needed if 2nd (?: # 2nd-edge (?: # target (?: (?P<nd_col>[A-Z]+|[_^.]) # A1-col (?P<nd_row>[123456789]\d*|[_^.]) # A1-row ) | (?: R(?P<nd_row2>-?[123456789]\d*|[_^.]) # RC-row C(?P<nd_col2>-?[123456789]\d*|[_^.]) # RC-col ) ) (?:\( (?P<nd_mov>L|U|R|D|LD|LU|UL|UR|RU|RD|DL|DR) # moves (?P<nd_mod>[+-])? # move-modifiers \) )? )? (?: :(?P<exp_moves>[LURD?123456789]+) # expansion moves )? )? (?: :\s*(?P<js_filt>[[{"].*) # filters )?$ """, re.IGNORECASE | re.X | re.DOTALL) """The regex for parsing regular :term:`xl-ref`. """
# TODO: Make exp_moves `?` work different from numbers. r""" ^(?P<moves>[LURD]+) # primitive moves (?P<times>\?|\d+)? # repetition times $""", re.IGNORECASE | re.X)
"""Excel use these !@#% chars for double-quotes, which are not valid JSON-strings!!"""
"""The :term:`call-specifier` for holding the parsed json-filters."""
"""Make optional the last 2 fields of :class:`CallSpec` ``(args', kwds)`` ."""
""" Parse :term:`call-specifier` from json-filters.
:param call_spec_values: This is a *non-null* structure specifying some function call in the `filter` part, which it can be either:
- string: ``"func_name"`` - list: ``["func_name", ["arg1", "arg2"], {"k1": "v1"}]`` where the last 2 parts are optional and can be given in any order; - object: ``{"func": "func_name", "args": ["arg1"], "kwds": {"k":"v"}}`` where the `args` and `kwds` are optional.
:return: the 3-tuple ``func, args=(), kwds={}`` with the defaults as shown when missing. """
isargs ^ iskwds ^ isnull).all():
else:
""" Returns an iterator that repeats `moves` x `times`, or infinite if unspecified.
Used when parsing primitive :term:`directions`.
:param str moves: the moves to repeat ie ``RU1D?`` :param str times: N of repetitions. If `None` it means infinite repetitions. :return: An iterator of the moves :rtype: iterator
Examples::
>>> list(_repeat_moves('LUR', '3')) ['LUR', 'LUR', 'LUR'] >>> list(_repeat_moves('ABC', '0')) [] >>> _repeat_moves('ABC') ## infinite repetitions repeat('ABC') """
""" Parse rect-expansion into a list of dir-letters iterables.
:param exp_moves: A string with a sequence of primitive moves: es. L1U1R1D1 :type xl_ref: str
:return: A list of primitive-dir chains. :rtype: list
Examples::
>>> res = _parse_expansion_moves('lu1urd?') >>> res [repeat('L'), repeat('U', 1), repeat('UR'), repeat('D', 1)]
# infinite generator >>> [next(res[0]) for i in range(10)] ['L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L', 'L']
>>> list(res[1]) ['U']
>>> _parse_expansion_moves('1LURD') Traceback (most recent call last): ValueError: Invalid rect-expansion(1LURD) due to: 'NoneType' object has no attribute 'groupdict'
"""
for v in res if v != '']
gs.pop('%s_mov' % prefix), gs.pop('%s_mod' % prefix), default_edge)
""" Parses a :term:`xl-ref` fragment(without '#').
:param str xlref_fragment: a string with the following format::
<sheet>!<st_col><st_row>(<st_mov>):<nd_col><nd_row>(<nd_mov>):<exp_moves>{<js_filt>}
i.e.::
sheet_name!UPT8(LU-):_.(D+):LDL1{"dims":1}
:return: dictionary containing the following parameters:
- sheet: (str, int, None) i.e. ``sheet_name`` - st_edge: (Edge, None) the 1st-ref, uncooked, with raw cell i.e. ``Edge(land=Cell(row='8', col='UPT'), mov='LU', mod='-')`` - nd_edge: (Edge, None) the 2nd-ref, uncooked, with raw cell i.e. ``Edge(land=Cell(row='_', col='.'), mov='D', mod='+')`` - exp_moves: (sequence, None), as i.e. ``LDL1`` parsed by :func:`_parse_expansion_moves()` - js_filt: dict i.e. ``{"dims: 1}``
:rtype: dict
Examples::
>>> res = _parse_xlref_fragment('Sheet1!A1(DR+):Z20(UL):L1U2R1D1:' ... '{"opts":{}, "func": "foo"}') >>> sorted(res.items()) [('call_spec', CallSpec(func='foo', args=[], kwds={})), ('exp_moves', 'L1U2R1D1'), ('nd_edge', Edge(land=Cell(row='20', col='Z'), mov='UL', mod=None)), ('opts', {}), ('sh_name', 'Sheet1'), ('st_edge', Edge(land=Cell(row='1', col='A'), mov='DR', mod='+'))]
Shortcut for all sheet from top-left to bottom-right full-cells::
>>> res=_parse_xlref_fragment(':') >>> sorted(res.items()) [('call_spec', None), ('exp_moves', None), ('nd_edge', Edge(land=Cell(row='_', col='_'), mov=None, mod=None)), ('opts', None), ('sh_name', None), ('st_edge', Edge(land=Cell(row='^', col='^'), mov=None, mod=None))]
Errors::
>>> _parse_xlref_fragment('A1(DR)Z20(UL)') Traceback (most recent call last): SyntaxError: Not an `xl-ref` syntax: A1(DR)Z20(UL)
"""
""" Parse a :term:`xl-ref` into a dict.
:param str xlref: a string with the following format::
<url_file>#<sheet>!<1st_edge>:<2nd_edge>:<expand><js_filt>
i.e.::
file:///path/to/file.xls#sheet_name!UPT8(LU-):_.(D+):LDL1{"dims":1}
:return: A dict with all fields, with None with those missing. :rtype: dict
Examples::
>>> res = parse_xlref('workbook.xlsx#Sheet1!A1(DR+):Z20(UL):L1U2R1D1:' ... '{"opts":{}, "func": "foo"}') >>> sorted(res.items()) [('call_spec', CallSpec(func='foo', args=[], kwds={})), ('exp_moves', 'L1U2R1D1'), ('nd_edge', Edge(land=Cell(row='20', col='Z'), mov='UL', mod=None)), ('opts', {}), ('sh_name', 'Sheet1'), ('st_edge', Edge(land=Cell(row='1', col='A'), mov='DR', mod='+')), ('url_file', 'workbook.xlsx'), ('xl_ref', 'workbook.xlsx#Sheet1!A1(DR+):Z20(UL):L1U2R1D1:{"opts":{}, "func": "foo"}')]
Shortcut for all sheet from top-left to bottom-right full-cells::
>>> res=parse_xlref('#:') >>> sorted(res.items()) [('call_spec', None), ('exp_moves', None), ('nd_edge', Edge(land=Cell(row='_', col='_'), mov=None, mod=None)), ('opts', None), ('sh_name', None), ('st_edge', Edge(land=Cell(row='^', col='^'), mov=None, mod=None)), ('url_file', None), ('xl_ref', '#:')]
Errors::
>>> parse_xlref('A1(DR)Z20(UL)') Traceback (most recent call last): SyntaxError: No fragment-part (starting with '#'): A1(DR)Z20(UL)
>>> parse_xlref('#A1(DR)Z20(UL)') ## Missing ':'. Traceback (most recent call last): SyntaxError: Not an `xl-ref` syntax: A1(DR)Z20(UL)
But as soon as syntax is matched, subsequent errors raised are :class:`ValueErrors`::
>>> parse_xlref("#A1:B1:{'Bad_JSON_str'}") Traceback (most recent call last): ValueError: Filters are not valid JSON: Expecting property name enclosed in double quotes: line 1 column 2 (char 1) JSON: {'Bad_JSON_str'} """
""" Returns top-left/bottom-down margins of full cells from a :term:`state` matrix.
May be used by :meth:`ABCSheet.get_margin_coords()` if a backend does not report the sheet-margins internally.
:param np.ndarray states_matrix: A 2D-array with `False` wherever cell are blank or empty. Use :meth:`ABCSheet.get_states_matrix()` to derrive it. :return: the 2 coords of the top-left & bottom-right full cells :rtype: (Coords, Coords)
Examples::
>>> states_matrix = np.asarray([ ... [0, 0, 0], ... [0, 1, 0], ... [0, 1, 1], ... [0, 0, 1], ... ]) >>> margins = _margin_coords_from_states_matrix(states_matrix) >>> margins (Coords(row=1, col=1), Coords(row=3, col=2))
Note that the botom-left cell is not the same as `states_matrix` matrix size::
>>> states_matrix = np.asarray([ ... [0, 0, 0, 0], ... [0, 1, 0, 0], ... [0, 1, 1, 0], ... [0, 0, 1, 0], ... [0, 0, 0, 0], ... ]) >>> _margin_coords_from_states_matrix(states_matrix) == margins True
"""
# return indices.min(0), indices.max(0)
""" Resolves special coords or converts Excel 1-based rows to zero-based, reporting invalids.
:param str, int coord: excel-row coordinate or one of ``^_.`` :return: excel row number, >= 0 :rtype: int
Examples::
>>> row = _row2num('1') >>> row 0 >>> row == _row2num(1) True
Negatives (from bottom) are preserved::
>>> _row2num('-1') -1
Fails ugly::
>>> _row2num('.') Traceback (most recent call last): ValueError: invalid literal for int() with base 10: '.' """
""" Resolves special coords or converts Excel A1 columns to a zero-based, reporting invalids.
:param str coord: excel-column coordinate or one of ``^_.`` :return: excel column number, >= 0 :rtype: int
Examples::
>>> col = _col2num('D') >>> col 3 >>> _col2num('d') == col True >>> _col2num('AaZ') 727 >>> _col2num('10') 9 >>> _col2num(9) 8
Negatives (from left-end) are preserved::
>>> _col2num('AaZ') 727
Fails ugly::
>>> _col2num('%$') Traceback (most recent call last): ValueError: substring not found
>>> _col2num([]) Traceback (most recent call last): TypeError: int() argument must be a string, a bytes-like object or a number, not 'list'
"""
else:
""" Translates special coords or converts Excel string 1-based rows/cols to zero-based, reporting invalids.
:param str cname: the coord-name, one of 'row', 'column' :param function cfunc: the function to convert coord ``str --> int`` :param int, str coord: the "A1" coord to translate :param int up_coord: the resolved *top* or *left* margin zero-based coordinate :param int dn_coord: the resolved *bottom* or *right* margin zero-based coordinate :param int, None base_coord: the resolved basis for dependent coord, if any
:return: the resolved coord or `None` if it were not a special coord.
Row examples::
>>> cname = 'row'
>>> r0 = _resolve_coord(cname, _row2num, '1', 1, 10) >>> r0 0 >>> r0 == _resolve_coord(cname, _row2num, 1, 1, 10) True >>> _resolve_coord(cname, _row2num, '^', 1, 10) 1 >>> _resolve_coord(cname, _row2num, '_', 1, 10) 10 >>> _resolve_coord(cname, _row2num, '.', 1, 10, 13) 13 >>> _resolve_coord(cname, _row2num, '-3', 0, 10) 8
But notice when base-cell missing::
>>> _resolve_coord(cname, _row2num, '.', 0, 10, base_coord=None) Traceback (most recent call last): ValueError: Cannot resolve `relative-row` without `base-coord`!
Other ROW error-checks::
>>> _resolve_coord(cname, _row2num, '0', 0, 10) Traceback (most recent call last): ValueError: invalid row('0') due to: Uncooked-coord cannot be zero!
>>> _resolve_coord(cname, _row2num, 'a', 0, 10) Traceback (most recent call last): ValueError: invalid row('a') due to: invalid literal for int() with base 10: 'a'
>>> _resolve_coord(cname, _row2num, None, 0, 10) Traceback (most recent call last): ValueError: invalid row(None) due to: int() argument must be a string, a bytes-like object or a number, not 'NoneType'
Column examples::
>>> cname = 'column'
>>> _resolve_coord(cname, _col2num, 'A', 1, 10) 0 >>> _resolve_coord(cname, _col2num, 'DADA', 1, 10) 71084 >>> _resolve_coord(cname, _col2num, '.', 1, 10, 13) 13 >>> _resolve_coord(cname, _col2num, '-4', 0, 10) 7
And COLUMN error-checks::
>>> _resolve_coord(cname, _col2num, None, 0, 10) Traceback (most recent call last): ValueError: invalid column(None) due to: int() argument must be a string, a bytes-like object or a number, not 'NoneType'
>>> _resolve_coord(cname, _col2num, 0, 0, 10) Traceback (most recent call last): ValueError: invalid column(0) due to: Uncooked-coord cannot be zero!
""" '^': up_coord, '_': dn_coord } else:
# Resolve negatives as from the end.
# fututils.raise_from(ValueError(msg.format(cname, coord, ex)), ex) see # GH 141
""" Translates any special coords to absolute ones.
To get the margin_coords, use one of:
* :meth:`ABCSheet.get_margin_coords()` * :func:`_margin_coords_from_states_matrix()`
:param Cell cell: The "A1" cell to translate its coords. :param Coords up_coords: the top-left resolved coords with full-cells :param Coords dn_coords: the bottom-right resolved coords with full-cells :param Coords base_cords: A resolved cell to base dependent coords (``.``). :return: the resolved cell-coords :rtype: Coords
Examples::
>>> up = Coords(1, 2) >>> dn = Coords(10, 6) >>> base = Coords(40, 50)
>>> _resolve_cell(Cell(col='B', row=5), up, dn) Coords(row=4, col=1)
>>> _resolve_cell(Cell('^', '^'), up, dn) Coords(row=1, col=2)
>>> _resolve_cell(Cell('_', '_'), up, dn) Coords(row=10, col=6)
>>> base == _resolve_cell(Cell('.', '.'), up, dn, base) True
>>> _resolve_cell(Cell('-1', '-2'), up, dn) Coords(row=10, col=5)
>>> _resolve_cell(Cell('A', 'B'), up, dn) Traceback (most recent call last): ValueError: invalid cell(Cell(row='A', col='B')) due to: invalid row('A') due to: invalid literal for int() with base 10: 'A'
But notice when base-cell missing::
>>> _resolve_cell(Cell('1', '.'), up, dn) Traceback (most recent call last): ValueError: invalid cell(Cell(row='1', col='.')) due to: Cannot resolve `relative-col` without `base-coord`!
""" else: up_coords[0], dn_coords[0], base_row) up_coords[1], dn_coords[1], base_col)
# fututils.raise_from(ValueError(msg % (cell, ex)), ex) see GH 141
# VECTO_SLICE REVERSE COORD_INDEX 'L': (1, -1, lambda r, c: (r, slice(None, c + 1))), 'U': (0, -1, lambda r, c: (slice(None, r + 1), c)), 'R': (1, 1, lambda r, c: (r, slice(c, None))), 'D': (0, 1, lambda r, c: (slice(r, None), c)), }
mov_slices=_mov_vector_slices): """Extract a slice from the states-matrix by starting from `land` and following `mov`."""
edge_name='', primitive_dir_vectors=_primitive_dir_vectors): """ Follow moves from `land` and stop on the 1st full-cell.
:param np.ndarray states_matrix: A 2D-array with `False` wherever cell are blank or empty. Use :meth:`ABCSheet.get_states_matrix()` to derrive it. :param Coords dn_coords: the bottom-right for the top-left of full-cells :param Coords land: the landing-cell :param str moves: MUST not be empty :return: the identified target-cell's coordinates :rtype: Coords
Examples::
>>> states_matrix = np.array([ ... [0, 0, 0, 0, 0, 0], ... [0, 0, 0, 0, 0, 0], ... [0, 0, 0, 1, 1, 1], ... [0, 0, 1, 0, 0, 1], ... [0, 0, 1, 1, 1, 1] ... ]) >>> args = (states_matrix, Coords(4, 5))
>>> _target_opposite(*(args + (Coords(0, 0), 'DR'))) Coords(row=3, col=2)
>>> _target_opposite(*(args + (Coords(0, 0), 'RD'))) Coords(row=2, col=3)
It fails if a non-empty target-cell cannot be found, or it ends-up beyond bounds::
>>> _target_opposite(*(args + (Coords(0, 0), 'D'))) Traceback (most recent call last): ValueError: No opposite-target found while moving(D) from landing-Coords(row=0, col=0)!
>>> _target_opposite(*(args + (Coords(0, 0), 'UR'))) Traceback (most recent call last): ValueError: No opposite-target found while moving(UR) from landing-Coords(row=0, col=0)!
But notice that the landing-cell maybe outside of bounds::
>>> _target_opposite(*(args + (Coords(3, 10), 'L'))) Coords(row=3, col=5)
"""
# if states_matrix[target].all(): # return Coords(*target)
# Limit negative coords, since they are valid indices. states_matrix, dn_coords, target, mov1) else:
""" :param np.ndarray states_matrix: A 2D-array with `False` wherever cell are blank or empty. Use :meth:`ABCSheet.get_states_matrix()` to derrive it. :param Coords dn_coords: the bottom-right for the top-left of full-cells :param Coords land: The landing-cell, which MUST be full! """ states_matrix, dn_coords, land, mov) else:
""" Scan term:`exterior` row and column on specified `moves` and stop on the last full-cell.
:param np.ndarray states_matrix: A 2D-array with `False` wherever cell are blank or empty. Use :meth:`ABCSheet.get_states_matrix()` to derrive it. :param Coords dn_coords: the bottom-right for the top-left of full-cells :param Coords land: the landing-cell which MUST be within bounds :param moves: which MUST not be empty :return: the identified target-cell's coordinates :rtype: Coords
Examples::
>>> states_matrix = np.array([ ... [0, 0, 0, 0, 0, 0], ... [0, 0, 0, 0, 0, 0], ... [0, 0, 0, 1, 1, 1], ... [0, 0, 1, 0, 0, 1], ... [0, 0, 1, 1, 1, 1] ... ]) >>> args = (states_matrix, Coords(4, 5))
>>> _target_same(*(args + (Coords(4, 5), 'U'))) Coords(row=2, col=5)
>>> _target_same(*(args + (Coords(4, 5), 'L'))) Coords(row=4, col=2)
>>> _target_same(*(args + (Coords(4, 5), 'UL', ))) Coords(row=2, col=2)
It fails if landing is empty or beyond bounds::
>>> _target_same(*(args + (Coords(2, 2), 'DR'))) Traceback (most recent call last): ValueError: No same-target found while moving(DR) from landing-Coords(row=2, col=2)!
>>> _target_same(*(args + (Coords(10, 3), 'U'))) Traceback (most recent call last): ValueError: No same-target found while moving(U) from landing-Coords(row=10, col=3)!
"""
np.asarray(land), mov)
""" Sorts rect-vertices in a 2D-array (with vertices in rows).
Example::
>>> _sort_rect((5, 3), (4, 6)) array([[4, 3], [5, 6]]) """
""" Applies the :term:`expansion-moves` based on the `states_matrix`.
:param state: :param Coords r1: any vertice of the rect to expand :param Coords r2: any vertice of the rect to expand :param np.ndarray states_matrix: A 2D-array with `False` wherever cell are blank or empty. Use :meth:`ABCSheet.get_states_matrix()` to derrive it. :param exp_moves: Just the parsed string, and not `None`. :return: a sorted rect top-left/bottom-right
Examples::
>>> states_matrix = np.array([ ... #0 1 2 3 4 5 ... [0, 0, 0, 0, 0, 0], #0 ... [0, 0, 1, 1, 1, 0], #1 ... [0, 1, 0, 0, 1, 0], #2 ... [0, 1, 1, 1, 1, 0], #3 ... [0, 0, 0, 0, 0, 1], #4 ... ], dtype=bool)
>>> r1, r2 = (Coords(2, 1), Coords(2, 1)) >>> _expand_rect(states_matrix, r1, r2, 'U') (Coords(row=2, col=1), Coords(row=2, col=1))
>>> r1, r2 = (Coords(3, 1), Coords(2, 1)) >>> _expand_rect(states_matrix, r1, r2, 'R') (Coords(row=2, col=1), Coords(row=3, col=4))
>>> r1, r2 = (Coords(2, 1), Coords(6, 1)) >>> _expand_rect(states_matrix, r1, r2, 'r') (Coords(row=2, col=1), Coords(row=6, col=5))
>>> r1, r2 = (Coords(2, 3), Coords(2, 3)) >>> _expand_rect(states_matrix, r1, r2, 'LURD') (Coords(row=1, col=1), Coords(row=3, col=4))
"""
'L': np.array([0, 0, -1, 0]), 'R': np.array([0, 0, 0, 1]), 'U': np.array([-1, 0, 0, 0]), 'D': np.array([0, 1, 0, 0]), } 'L': [0, 1, 2, 2], 'R': [0, 1, 3, 3], 'U': [0, 0, 2, 3], 'D': [1, 1, 2, 3], }
# Sort rect's vertices top-left/bottom-right. # # ``[r1, r2, c1, c2]`` to use slices, below slice(*exp_vect_i[2:])]
st_edge, nd_edge=None, exp_moves=None, base_coords=None): """ Performs :term:`targeting`, :term:`capturing` and :term:`expansions` based on the :term:`states-matrix`.
To get the margin_coords, use one of:
* :meth:`ABCSheet.get_margin_coords()` * :func:`_margin_coords_from_states_matrix()`
Its results can be fed into :func:`read_capture_values()`.
:param np.ndarray states_matrix: A 2D-array with `False` wherever cell are blank or empty. Use :meth:`ABCSheet.get_states_matrix()` to derrive it. :param (Coords, Coords) up_dn_margins: the top-left/bottom-right coords with full-cells :param Edge st_edge: "uncooked" as matched by regex :param Edge nd_edge: "uncooked" as matched by regex :param list or none exp_moves: Just the parsed string, and not `None`. :param Coords base_coords: The base for a :term:`dependent` :term;`1st` edge.
:return: a ``(Coords, Coords)`` with the 1st and 2nd :term:`capture-cell` ordered from top-left --> bottom-right. :rtype: tuple
Examples::
>>> states_matrix = np.array([ ... [0, 0, 0, 0, 0, 0], ... [0, 0, 0, 0, 0, 0], ... [0, 0, 0, 1, 1, 1], ... [0, 0, 1, 0, 0, 1], ... [0, 0, 1, 1, 1, 1] ... ], dtype=bool) >>> up, dn = _margin_coords_from_states_matrix(states_matrix)
>>> st_edge = Edge(Cell('1', 'A'), 'DR') >>> nd_edge = Edge(Cell('.', '.'), 'DR') >>> resolve_capture_rect(states_matrix, (up, dn), st_edge, nd_edge) (Coords(row=3, col=2), Coords(row=4, col=2))
Using dependenent coordinates for the 2nd edge::
>>> st_edge = Edge(Cell('_', '_'), None) >>> nd_edge = Edge(Cell('.', '.'), 'UL') >>> rect = resolve_capture_rect(states_matrix, (up, dn), st_edge, nd_edge) >>> rect (Coords(row=2, col=2), Coords(row=4, col=5))
Using sheet's margins::
>>> st_edge = Edge(Cell('^', '_'), None) >>> nd_edge = Edge(Cell('_', '^'), None) >>> rect == resolve_capture_rect(states_matrix, (up, dn), st_edge, nd_edge) True
Walking backwards::
>>> st_edge = Edge(Cell('^', '_'), 'L') # Landing is full, so 'L' ignored. >>> nd_edge = Edge(Cell('_', '_'), 'L', '+') # '+' or would also stop. >>> rect == resolve_capture_rect(states_matrix, (up, dn), st_edge, nd_edge) True
"""
'1st-') else: '1st-')
else:
nd_edge.land == Cell('.', '.') and nd_edge.mod != '-'): states_matrix, dn_margin, nd, mov, '2nd-') else: states_matrix, dn_margin, nd, mov, '2nd-')
else:
""" Identifies rect from its edge-coordinates (row, col, 2d-table)..
:param Coords st: the top-left edge of capture-rect, inclusive :param Coords or None nd: the bottom-right edge of capture-rect, inclusive :return: in int based on the input like that:
- 0: only `st` given - 1: `st` and `nd` point the same cell - 2: row - 3: col - 4: 2d-table
Examples::
>>> _classify_rect_shape((1,1), None) 0 >>> _classify_rect_shape((2,2), (2,2)) 1 >>> _classify_rect_shape((2,2), (2,20)) 2 >>> _classify_rect_shape((2,2), (20,2)) 3 >>> _classify_rect_shape((2,2), (20,20)) 4 """
""" Append trivial dimensions to the left.
:param values: The scalar ot 2D-results of :meth:`Sheet.read_rect()` :param int new_dim: The new dimension the result should have """
""" Squeeze it, and then flatten it, before inflating it.
:param values: The scalar ot 2D-results of :meth:`Sheet.read_rect()` :param int new_dim: The new dimension the result should have """ else:
""" Reshapes the :term:`capture-rect` values of :func:`read_capture_rect()`.
:param values: The scalar ot 2D-results of :meth:`Sheet.read_rect()` :type values: (nested) list, * :param new_ndim: :type int, (int, bool) or None new_ndim:
:return: reshaped values :rtype: list of lists, list, *
Examples::
>>> _redim([1, 2], 2) [[1, 2]]
>>> _redim([[1, 2]], 1) [1, 2]
>>> _redim([], 2) [[]]
>>> _redim([[3.14]], 0) 3.14
>>> _redim([[11, 22]], 0) [11, 22]
>>> arr = [[[11], [22]]] >>> arr == _redim(arr, None) True
>>> _redim([[11, 22]], 0) [11, 22] """
""" A caching-store of :class:`ABCSheet` instances, serving them based on (workbook, sheet) IDs, optionally creating them from backends.
:ivar dict _cached_sheets: A cache of all _Spreadsheets accessed so far, keyed by multiple keys generated by :meth:`_derive_sheet_keys`. :ivar ABCSheet _current_sheet: The last used sheet, used when unspecified by the xlref.
- To avoid opening non-trivial workbooks, use the :meth:`add_sheet()` to pre-populate this cache with them.
- The last sheet added becomes the *current-sheet*, and will be served when :term:`xl-ref` does not specify any workbook and sheet.
.. Tip:: For the simplest API usage, try this::
>>> sf = SheetsFactory() >>> sf.add_sheet(some_sheet) # doctest: +SKIP >>> lasso('A1:C3(U)', sf) # doctest: +SKIP
- The *current-sheet* is served only when wokbook-id is `None`, that is, the id-pair ``('foo.xlsx', None)`` does not hit it, so those ids are send to the cache as they are.
- To add another backend, modify the opening-sheets logic (ie clipboard), override :meth:`_open_sheet()`.
- It is a resource-manager for contained sheets, wo it can be used wth a `with` statement.
"""
else:
""" Retuns the product of user-specified and sheet-internal keys.
:param wb_ids: a single or a sequence of extra workbook-ids (ie: file, url) :param sh_ids: a single or sequence of extra sheet-ids (ie: name, index, None) """
for p in key_pairs if p[0] is not None))
"""Closes all contained sheets and empties cache."""
no_current=False): """ Updates cache and (optionally) `_current_sheet`.
:param wb_ids: a single or sequence of extra workbook-ids (ie: file, url) :param sh_ids: a single or sequence of extra sheet-ids (ie: name, index, None) """
else:
"""OVERRIDE THIS to change backend."""
('xl_ref', 'url_file', 'sh_name', 'st_edge', 'nd_edge', 'exp_moves', 'call_spec', 'sheet', 'st', 'nd', 'values', 'base_cell', 'opts')) """ All the fields used by the algorithm, populated stage-by-stage by :class:`Ranger`.
:param str xl_ref: The full url, populated on parsing. :param str sh_name: Parsed sheet name (or index, but still as string), populated on parsing. :param Edge st_edge: The 1st edge, populated on parsing. :param Edge nd_edge: The 2nd edge, populated on parsing. :param Coords st: The top-left targeted coords of the :term:`capture-rect`, populated on :term:`capturing`.` :param Coords nd: The bottom-right targeted coords of the :term:`capture-rect`, populated on :term:`capturing` :param ABCSheet sheet: The fetched from factory or ranger's current sheet, populated after :term:`capturing` before reading. :param values: The excel's table-values captured by the :term:`lasso`, populated after reading updated while applying :term:`filters`. :param dict or ChainMap opts: - Before `parsing`, they are just any 'opts' dict found in the :term:`filters`. - After *parsing, a 2-map ChainMap with :attr:`Ranger.base_opts` and options extracted from *filters* on top. """
"""Make :class:`Lasso` construct with all missing fields as `None`."""
""" The director-class that performs all stages required for "throwing the lasso" around rect-values.
Use it when you need to have total control of the procedure and configuration parameters, since no defaults are assumed.
The :meth:`do_lasso()` does the job.
:ivar SheetsFactory sheets_factory: Factory of sheets from where to parse rect-values; does not close it in the end. Maybe `None`, but :meth:`do_lasso()` will scream unless invoked with a `context_lasso` arg containing a concrete :class:`ABCSheet`. :ivar dict base_opts: The :term:`opts` that are deep-copied and used as the defaults for every :meth:`do_lasso()`, whether invoked directly or recursively by :meth:`recursive_filter()`. If unspecified, no opts are used, but this attr is set to an empty dict. See :func:`get_default_opts()`. :ivar dict or None available_filters: No filters exist if unspecified. See :func:`get_default_filters()`. :ivar Lasso intermediate_lasso: A ``('stage', Lasso)`` pair with the last :class:`Lasso` instance produced during the last execution of the :meth:`do_lasso()`. Used for inspecting/debuging. :ivar _context_lasso_fields: The name of the fields taken from `context_lasso` arg of :meth:`do_lasso()`, when the parsed ones are `None`. Needed for recursive invocations, see :meth:`recursive_filter`. """
base_opts=None, available_filters=None):
"""Replace lasso-values and updated :attr:`intermediate_lasso`."""
# Just to update intermediate_lasso.
msg, func_name, args, kwds, ex, help_msg, exc_info=1) else:
""" Apply all call-specifiers one after another on the captured values.
:param list pipe: the call-specifiers """
""" Recursively expand any :term:`xl-ref` strings found by treating values as mappings (dicts, df, series) and/or nested lists.
- The `include`/`exclude` filter args work only for dict-like objects with ``items()`` or ``iteritems()`` and indexing methods, i.e. Mappings, series and dataframes.
- If no filter arg specified, expands for all keys. - If only `include` specified, rejects all keys not explicitly contained in this filter arg. - If only `exclude` specified, expands all keys not explicitly contained in this filter arg. - When both `include`/`exclude` exist, only those explicitely included are accepted, unless also excluded.
- Lower the :mod:`logging` level to see other than syntax-errors on recursion reposrted on :data:`log`; .
:param list or str include: Items to include in the recursive-search. See descritpion above. :param list or str exclude: Items to include in the recursive-search. See descritpion above. :param int or None depth: How deep to dive into nested structures for parsing xl-refs. If `< 0`, no limit. If 0, stops completely. """
else: # No base_cell possible with Indexed.
"""Creates the lasso to be used for each new :meth:`do_lasso()` invocation."""
""" Merges xl-ref parsed-parsed_fields with `init_lasso`, reporting any errors.
:param Lasso init_lasso: Default values to be overridden by non-nulls. Note that ``init_lasso.opts`` must be a `ChainMap`, as returned by :math:`_make_init_Lasso()`.
:return: a Lasso with any non `None` parsed-fields updated """
parsed_fields) # raise fututils.raise_from(ValueError(msg % (xlref, ex)), ex) see GH # 141
lasso.url_file, lasso.sh_name, lasso.opts) lasso.url_file, lasso.sh_name, lasso.opts) # Maybe context had a Sheet already.
lasso.st_edge, lasso.nd_edge, lasso.exp_moves)
""" The director-method that does all the job of hrowing a :term:`lasso` around spreadsheet's rect-regions according to :term:`xl-ref`.
:param str xlref: a string with the :term:`xl-ref` format::
<url_file>#<sheet>!<1st_edge>:<2nd_edge>:<expand><js_filt>
i.e.::
file:///path/to/file.xls#sheet_name!UPT8(LU-):_.(D+):LDL1{"dims":1}
:param Lasso context_kwds: Default :class:`Lasso` fields in case parsed ones are `None` Only those in :attr:`_context_lasso_fields` are taken into account. Utilized by :meth:`recursive_filter()`. :return: The final :class:`Lasso` with captured & filtered values. :rtype: Lasso """
st=st, nd=nd, base_cell=lasso.base_cell)
# relasso() internally
############### # FILTER-DEFS ###############
"""A list :term:`call-spec` for :meth:`_redim_filter` :term:`filter` that imitates results of *xlwings* library."""
scalar=None, cell=None, row=None, col=None, table=None): """ Reshape and/or transpose captured values, depending on rect's shape.
Each dimension might be a single int or None, or a pair [dim, transpose]. """
""" The default available :term:`filters` used by :func:`lasso()` when constructing its internal :class:`Ranger`.
:param dict or None overrides: Any items to update the default ones.
:return: a dict-of-dicts with 2 items:
- *func*: a function with args: ``(Ranger, Lasso, *args, **kwds)`` - *desc*: help-text replaced by ``func.__doc__`` if missing.
:rtype: dict """ 'pipe': { 'func': Ranger.pipe_filter, }, 'recurse': { 'func': Ranger.recursive_filter, }, 'redim': { 'func': redim_filter, }, 'numpy': { 'func': lambda ranger, lasso, * args, **kwds: lasso._replace( values=np.array(lasso.values, *args, **kwds)), 'desc': np.array.__doc__, }, 'dict': { 'func': lambda ranger, lasso, * args, **kwds: lasso._replace( values=dict(lasso.values, *args, **kwds)), 'desc': dict.__doc__, }, 'odict': { 'func': lambda ranger, lasso, * args, **kwds: lasso._replace( values=OrderedDict(lasso.values, *args, **kwds)), 'desc': OrderedDict.__doc__, }, 'sorted': { 'func': lambda ranger, lasso, * args, **kwds: lasso._replace( values=sorted(lasso.values, *args, **kwds)), 'desc': sorted.__doc__, }, }
'names') is None else None # , convert_float=True,
'df': { 'func': _df_filter, 'desc': parsers.TextParser.__doc__, }, 'series': { 'func': lambda ranger, lasso, *args, **kwds: pd.Series(OrderedDict(lasso.values), *args, **kwds), 'desc': ("Converts a 2-columns list-of-lists into pd.Series.\n" + pd.Series.__doc__), } }) except ImportError as ex: msg = "The 'df' and 'series' filters were notinstalled, due to: %s" log.info(msg, ex)
""" Default :term:`opts` used by :func:`lasso()` when constructing its internal :class:`Ranger`.
:param dict or None overrides: Any items to update the default ones. """ 'lax': False, 'verbose': False, 'read': {'on_demand': True, }, }
base_opts=None, available_filters=None): """ Makes a defaulted :class:`Ranger`.
:param sheets_factory: Factory of sheets from where to parse rect-values; if unspecified, a new :class:`SheetsFactory` is created. Remember to invoke its :meth:`SheetsFactory.close()` to clear resources from any opened sheets. :param dict or None base_opts: Default opts to affect the lassoing, to be merged with defaults; uses :func:`get_default_opts()`.
Read the code to be sure what are the available choices :-(. :param dict or None available_filters: The :term:`filters` available to xlrefs, to be merged with defaults;. Uses :func:`get_default_filters()` if unspecified.
""" base_opts or get_default_opts(), available_filters or get_default_filters())
sheets_factory=None, base_opts=None, available_filters=None, return_lasso=False, **context_kwds): """ High-level function to :term:`lasso` around spreadsheet's rect-regions according to :term:`xl-ref` strings by using internally a :class:`Ranger` .
:param str xlref: a string with the :term:`xl-ref` format::
<url_file>#<sheet>!<1st_edge>:<2nd_edge>:<expand><js_filt>
i.e.::
file:///path/to/file.xls#sheet_name!UPT8(LU-):_.(D+):LDL1{"dims":1}
:param sheets_factory: Factory of sheets from where to parse rect-values; if unspecified, the new :class:`SheetsFactory` created is closed afterwards. Delegated to :func:`make_default_Ranger()`, so items override default ones; use a new :class:`Ranger` if that is not desired. :ivar dict or None base_opts: Opts affecting the lassoing procedure that are deep-copied and used as the base-opts for every :meth:`Ranger.do_lasso()`, whether invoked directly or recursively by :meth:`Ranger.recursive_filter()`. Read the code to be sure what are the available choices. Delegated to :func:`make_default_Ranger()`, so items override default ones; use a new :class:`Ranger` if that is not desired. :param dict or None available_filters: Delegated to :func:`make_default_Ranger()`, so items override default ones; use a new :class:`Ranger` if that is not desired. :param bool return_lasso: If `True`, values are contained in the returned Lasso instance, along with all other artifacts of the :term:`lassoing` procedure.
For more debugging help, create a :class:`Range` yourself and inspect the :attr:`Ranger.intermediate_lasso`. :param Lasso context_kwds: Default :class:`Lasso` fields in case parsed ones are `None` (i.e. you can specify the sheet like that). Only those in :attr:`Ranger._context_lasso_fields` are taken into account.
:return: Either the captured & filtered values or the final :class:`Lasso`, depending on the `return_lassos` arg. """
base_opts=base_opts, available_filters=available_filters) finally:
""" A delegating to backend factory and sheet-wrapper with utility methods.
:param np.ndarray _states_matrix: The :term:`states-matrix` cached, so recreate object to refresh it. :param dict _margin_coords: limits used by :func:`_resolve_cell`, cached, so recreate object to refresh it.
Resource management is outside of the scope of this class, and must happen in the backend workbook/sheet instance.
*xlrd* examples::
>>> import xlrd # doctest: +SKIP >>> with xlrd.open_workbook(self.tmp) as wb: # doctest: +SKIP ... sheet = xlref.xlrdSheet(wb.sheet_by_name('Sheet1')) ... ## Do whatever
*win32* examples::
>>> with dsgdsdsfsd as wb: # doctest: +SKIP ... sheet = xlref.win32Sheet(wb.sheet['Sheet1']) TODO: Win32 Sheet example """
""" Override it to release resources for this sheet."""
""" Override it to release resources this and all sibling sheets."""
def get_sheet_ids(self): """ :return: a 2-tuple of its wb-name and a sheet-ids of this sheet i.e. name & indx :rtype: ([str or None, [str or int or None]) """
"""Return a sibling sheet by the given index or name"""
def _read_states_matrix(self): """ Read the :term:`states-matrix` of the wrapped sheet.
:return: A 2D-array with `False` wherever cell are blank or empty. :rtype: ndarray """
""" Read and cache the :term:`states-matrix` of the wrapped sheet.
:return: A 2D-array with `False` wherever cell are blank or empty. :rtype: ndarray """
def read_rect(self, st, nd): """ Fecth the actual values from the backend Excel-sheet.
:param Coords st: the top-left edge, inclusive :param Coords, None nd: the bottom-right edge, inclusive(!); when `None`, must return a scalar value. :return: a 1D or 2D-list with the values fenced by the rect, which might be empty if beyond limits. :rtype: list """
""" Override if possible to read (any of the) limits directly from the sheet.
:return: the 2 coords of the top-left & bottom-right full cells; anyone coords can be None. By default returns ``(None, None)``. :rtype: (Coords, Coords)
""" return None, None # pragma: no cover
""" Extract (and cache) margins either internally or from :func:`_margin_coords_from_states_matrix()`.
:return: the resolved top-left and bottom-right :class:`Coords` :rtype: tuple
"""
"""A sample :class:`ABCSheet` made out of 2D-list or numpy-arrays, for facilitating tests."""
raise NotImplementedError()
|