From da737286c34a3c09b3e95bc53e1b8d5e31e063d5 Mon Sep 17 00:00:00 2001 From: Alix Damman Date: Thu, 20 Feb 2020 09:48:39 +0100 Subject: [PATCH 1/2] fix #567 : - dropped Python 2 support - deleted compat.py module - abc classes now inherit from abc.ABC in abstractbases.py module --- .travis.yml | 11 +- README.rst | 2 +- condarecipe/larray/conda_build_config.yaml | 1 - doc/source/changes/version_0_33.rst.inc | 2 +- doc/source/contribute.rst | 3 - larray/__init__.py | 2 - larray/core/abstractbases.py | 15 +-- larray/core/array.py | 73 ++++-------- larray/core/axis.py | 75 ++++-------- larray/core/constants.py | 2 - larray/core/expr.py | 3 - larray/core/group.py | 117 ++++++++---------- larray/core/metadata.py | 131 ++++----------------- larray/core/session.py | 42 ++++--- larray/example.py | 2 +- larray/inout/common.py | 6 +- larray/inout/csv.py | 5 +- larray/inout/excel.py | 2 - larray/inout/hdf.py | 2 - larray/inout/misc.py | 3 +- larray/inout/pandas.py | 23 ++-- larray/inout/pickle.py | 4 +- larray/inout/sas.py | 2 - larray/inout/session.py | 6 +- larray/inout/stata.py | 2 - larray/inout/xw_excel.py | 15 +-- larray/inout/xw_reporting.py | 6 +- larray/ipfp/__init__.py | 2 - larray/tests/common.py | 2 - larray/tests/test_array.py | 47 ++++---- larray/tests/test_axis.py | 3 - larray/tests/test_axiscollection.py | 2 - larray/tests/test_example.py | 2 - larray/tests/test_excel.py | 2 - larray/tests/test_group.py | 3 - larray/tests/test_ipfp.py | 2 - larray/tests/test_options.py | 1 - larray/tests/test_session.py | 6 +- larray/util/compat.py | 58 --------- larray/util/misc.py | 5 +- larray/util/options.py | 1 - larray/viewer/__init__.py | 2 - setup.py | 2 - 43 files changed, 202 insertions(+), 495 deletions(-) delete mode 100644 larray/util/compat.py diff --git a/.travis.yml b/.travis.yml index 9c6bfa121..5ad1b61ae 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,6 @@ dist: xenial # required for Python >= 3.7 language: python python: - - "2.7" - "3.6" - "3.7" @@ -17,20 +16,12 @@ before_install: # Install Miniconda # We do this conditionally because it saves us some downloading if the # version is the same. - - if [[ "$TRAVIS_PYTHON_VERSION" == "2.7" ]]; then - wget https://repo.continuum.io/miniconda/Miniconda-latest-Linux-x86_64.sh -O miniconda.sh; - else - wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh; - fi + - wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh; - bash miniconda.sh -b -p $HOME/miniconda - export PATH="$HOME/miniconda/bin:$PATH" - hash -r - conda config --add channels conda-forge - conda config --set always_yes yes --set changeps1 no - # workaround for conda >= 4.8 - - if [[ "$TRAVIS_PYTHON_VERSION" == "2.7" ]]; then - pip install tqdm; - fi - conda update -q conda # Useful for debugging any issues with conda diff --git a/README.rst b/README.rst index f15967fa7..8c04ecf96 100644 --- a/README.rst +++ b/README.rst @@ -73,7 +73,7 @@ Once you have satisfied the requirements detailed below, simply run:: Required Dependencies --------------------- -- Python 2.7, 3.6 or 3.7 +- Python 3.6 or 3.7 - `numpy `__ (1.13 or later) - `pandas `__ (0.20 or later) diff --git a/condarecipe/larray/conda_build_config.yaml b/condarecipe/larray/conda_build_config.yaml index e59e77303..c3ae13fbe 100644 --- a/condarecipe/larray/conda_build_config.yaml +++ b/condarecipe/larray/conda_build_config.yaml @@ -1,5 +1,4 @@ python: - - 2.7 - 3.5 - 3.6 - 3.7 diff --git a/doc/source/changes/version_0_33.rst.inc b/doc/source/changes/version_0_33.rst.inc index 522358af8..be8409d99 100644 --- a/doc/source/changes/version_0_33.rst.inc +++ b/doc/source/changes/version_0_33.rst.inc @@ -12,7 +12,7 @@ Syntax changes Backward incompatible changes ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -* other backward incompatible changes +* dropped support for Python 2 (closes :issue:`567`). New features diff --git a/doc/source/contribute.rst b/doc/source/contribute.rst index 735eea788..636c2148a 100644 --- a/doc/source/contribute.rst +++ b/doc/source/contribute.rst @@ -181,9 +181,6 @@ code conventions. Among others, this means: This summary should not prevent you from reading the PEP! -LArray is currently compatible with both Python 2 and 3. -So make sure your code is compatible with both versions. - Step 3: Document your code ~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/larray/__init__.py b/larray/__init__.py index 3006e6bc8..f02766cec 100644 --- a/larray/__init__.py +++ b/larray/__init__.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, division, print_function - __version__ = '0.33-dev' diff --git a/larray/core/abstractbases.py b/larray/core/abstractbases.py index 599e74975..07e80e57b 100644 --- a/larray/core/abstractbases.py +++ b/larray/core/abstractbases.py @@ -1,18 +1,15 @@ -from __future__ import absolute_import - -from abc import ABCMeta +from abc import ABC # define abstract base classes to enable isinstance type checking on our objects # idea taken from https://github.com/pandas-dev/pandas/blob/master/pandas/core/dtypes/generic.py -# FIXME: __metaclass__ is ignored in Python 3 -class ABCAxis(object): - __metaclass__ = ABCMeta +class ABCAxis(ABC): + pass class ABCAxisReference(ABCAxis): - __metaclass__ = ABCMeta + pass -class ABCArray(object): - __metaclass__ = ABCMeta +class ABCArray(ABC): + pass diff --git a/larray/core/array.py b/larray/core/array.py index 103f6d869..9881779e5 100644 --- a/larray/core/array.py +++ b/larray/core/array.py @@ -1,6 +1,3 @@ -# -*- coding: utf8 -*- -from __future__ import absolute_import, division, print_function - """ Array class """ @@ -30,6 +27,8 @@ from collections import OrderedDict from itertools import product, chain, groupby, islice, repeat +from collections.abc import Iterable, Sequence +import builtins import os import sys import functools @@ -59,7 +58,6 @@ float_error_handler_factory, light_product, common_type, renamed_to, deprecate_kwarg, LHDFStore, lazy_attribute, unique_multi, SequenceZip, Repeater, Product, ensure_no_numpy_type) -from larray.util.compat import PY2, basestring, Iterable, Sequence, builtins from larray.util.options import _OPTIONS, DISPLAY_MAXLINES, DISPLAY_EDGEITEMS, DISPLAY_WIDTH, DISPLAY_PRECISION @@ -305,42 +303,23 @@ def concat(arrays, axis=0, dtype=None): return result -if PY2: - class ArrayIterator(object): - __slots__ = ('next',) - - def __init__(self, array): - data_iter = iter(array.data) - next_data_func = data_iter.next - res_axes = array.axes[1:] - # this case should not happen (handled by the fastpath in Array.__iter__) - assert len(res_axes) > 0 - - def next_func(): - return Array(next_data_func(), res_axes) +class ArrayIterator(object): + __slots__ = ('__next__',) - self.next = next_func - - def __iter__(self): - return self -else: - class ArrayIterator(object): - __slots__ = ('__next__',) - - def __init__(self, array): - data_iter = iter(array.data) - next_data_func = data_iter.__next__ - res_axes = array.axes[1:] - # this case should not happen (handled by the fastpath in Array.__iter__) - assert len(res_axes) > 0 + def __init__(self, array): + data_iter = iter(array.data) + next_data_func = data_iter.__next__ + res_axes = array.axes[1:] + # this case should not happen (handled by the fastpath in Array.__iter__) + assert len(res_axes) > 0 - def next_func(): - return Array(next_data_func(), res_axes) + def next_func(): + return Array(next_data_func(), res_axes) - self.__next__ = next_func + self.__next__ = next_func - def __iter__(self): - return self + def __iter__(self): + return self # TODO: rename to ArrayIndexIndexer or something like that @@ -826,7 +805,7 @@ def title(self): def title(self, title): import warnings warnings.warn("title attribute is deprecated. Please use meta.title instead", FutureWarning, stacklevel=2) - if not isinstance(title, basestring): + if not isinstance(title, str): raise TypeError("Expected string value, got {}".format(type(title).__name__)) self._meta.title = title @@ -1411,8 +1390,6 @@ def __array_wrap__(self, out_arr, context=None): def __bool__(self): return bool(self.data) - # Python 2 - __nonzero__ = __bool__ # TODO: either support a list (of axes names) as first argument here (and set_labels) # or don't support that in set_axes @@ -1591,7 +1568,7 @@ def reindex(self, axes_to_reindex=None, new_axis=None, fill_value=nan, inplace=F a1 c0 2 1 """ # XXX: can't we move this to AxisCollection.replace? - if isinstance(axes_to_reindex, basestring) and '=' in axes_to_reindex: + if isinstance(axes_to_reindex, str) and '=' in axes_to_reindex: axes_to_reindex = Axis(axes_to_reindex) if isinstance(axes_to_reindex, (Group, Axis)) and not isinstance(axes_to_reindex, AxisReference): new_axis = axes_to_reindex if isinstance(axes_to_reindex, Axis) else Axis(axes_to_reindex) @@ -2820,7 +2797,7 @@ def to_labelgroup(key, stack_depth=1): if not all(g.axis.equals(axis) for g in groups[1:]): raise ValueError("group with different axes: %s" % str(key)) return groups - if isinstance(key, (Group, int, basestring, list, slice)): + if isinstance(key, (Group, int, str, list, slice)): return self.axes._guess_axis(key) else: raise NotImplementedError("%s has invalid type (%s) for a group aggregate key" @@ -7744,10 +7721,10 @@ def split_axes(self, axes=None, sep='_', names=None, regex=None, sort=False, fil # * somehow factorize this code with AxisCollection.split_axes if axes is None: axes = {axis: None for axis in array.axes if axis.name is not None and sep in axis.name} - elif isinstance(axes, (int, basestring, Axis)): + elif isinstance(axes, (int, str, Axis)): axes = {axes: names} elif isinstance(axes, (list, tuple)): - if all(isinstance(axis, (int, basestring, Axis)) for axis in axes): + if all(isinstance(axis, (int, str, Axis)) for axis in axes): axes = {axis: None for axis in axes} else: raise ValueError("Expected tuple or list of int, string or Axis instances") @@ -9288,12 +9265,12 @@ def stack(elements=None, axes=None, title=None, meta=None, dtype=None, res_axes= if elements is not None and kwargs: raise TypeError("stack() accepts either keyword arguments OR a collection of elements, not both") - if isinstance(axes, basestring) and '=' in axes: + if isinstance(axes, str) and '=' in axes: axes = Axis(axes) elif isinstance(axes, Group): axes = Axis(axes) - if axes is not None and not isinstance(axes, basestring): + if axes is not None and not isinstance(axes, str): axes = AxisCollection(axes) if kwargs: @@ -9321,7 +9298,7 @@ def stack(elements=None, axes=None, title=None, meta=None, dtype=None, res_axes= if all(isinstance(e, tuple) for e in elements): assert all(len(e) == 2 for e in elements) - if axes is None or isinstance(axes, basestring): + if axes is None or isinstance(axes, str): keys = [k for k, v in elements] values = [v for k, v in elements] # assert that all keys are indexers @@ -9338,7 +9315,7 @@ def translate_and_sort_key(key, axes): dict_elements = {translate_and_sort_key(key, axes): value for key, value in elements} items = [(k, dict_elements[k]) for k in axes.iter_labels()] else: - if axes is None or isinstance(axes, basestring): + if axes is None or isinstance(axes, str): axes = AxisCollection(Axis(len(elements), axes)) else: # TODO: add support for more than one axis here @@ -9568,7 +9545,7 @@ def values_with_expand(value, axes, readonly=True, ascending=True): if not isinstance(axes, (tuple, list, AxisCollection)): axes = (axes,) # transform string axes definitions to objects - axes = [Axis(axis) if isinstance(axis, basestring) and '=' in axis else axis + axes = [Axis(axis) if isinstance(axis, str) and '=' in axis else axis for axis in axes] # transform string axes references to objects axes = AxisCollection([axis if isinstance(axis, Axis) else all_axes[axis] diff --git a/larray/core/axis.py b/larray/core/axis.py index c203e4e7d..404415a66 100644 --- a/larray/core/axis.py +++ b/larray/core/axis.py @@ -1,6 +1,3 @@ -# -*- coding: utf8 -*- -from __future__ import absolute_import, division, print_function - import fnmatch import re import sys @@ -17,7 +14,6 @@ from larray.util.oset import * from larray.util.misc import (duplicates, array_lookup2, ReprString, index_by_id, renamed_to, common_type, LHDFStore, lazy_attribute, _isnoneslice, unique_multi, Product) -from larray.util.compat import (basestring, PY2, unicode, long) np_frompyfunc = np.frompyfunc @@ -87,7 +83,7 @@ def __init__(self, labels, name=None): name = labels.axis if isinstance(name, Axis): name = name.name - if isinstance(labels, basestring): + if isinstance(labels, str): if '=' in labels: name, labels = [o.strip() for o in labels.split('=')] elif '..' not in labels and ',' not in labels: @@ -98,10 +94,10 @@ def __init__(self, labels, name=None): # make sure we do not have np.str_ as it causes problems down the # line with xlwings. Cannot use isinstance to check that though. - name_is_python_str = type(name) is unicode or type(name) is bytes - if isinstance(name, basestring) and not name_is_python_str: - name = unicode(name) - if name is not None and not isinstance(name, (int, basestring)): + name_is_python_str = type(name) is str or type(name) is bytes + if isinstance(name, str) and not name_is_python_str: + name = str(name) + if name is not None and not isinstance(name, (int, str)): raise TypeError("Axis name should be None, int or str but is: %s (%s)" % (name, type(name).__name__)) self.name = name self._labels = None @@ -189,7 +185,7 @@ def labels(self): def labels(self, labels): if labels is None: raise TypeError("labels should be a sequence or a single int, not None") - if isinstance(labels, (int, long, np.integer)): + if isinstance(labels, (int, np.integer)): length = labels labels = np.arange(length) iswildcard = True @@ -783,34 +779,11 @@ def __contains__(self, key): def __hash__(self): return id(self) - # needed for Python 2 only (on Python2 all classes defining __slots__ need to define __getstate__/__setstate__ too) - def __getstate__(self): - return self.name, self._length if self._iswildcard else self._labels - - # needed for Python 2 only - def __setstate__(self, state): - name, labels = state - self.name = name - self._labels = None - self._length = None - self._iswildcard = False - - self.__mapping = None - self.__sorted_keys = None - self.__sorted_values = None - # set _labels, _length and _iswildcard via the property - self.labels = labels - def _is_key_type_compatible(self, key): key_kind = np.dtype(type(key)).kind label_kind = self.labels.dtype.kind - # on Python2, ascii-only unicode string can match byte strings (and vice versa), so we shouldn't be more picky - # here than dict hashing - str_key = key_kind in ('S', 'U') - str_label = label_kind in ('S', 'U') - py2_str_match = PY2 and str_key and str_label # object kind can match anything - return key_kind == label_kind or key_kind == 'O' or label_kind == 'O' or py2_str_match + return key_kind == label_kind or key_kind == 'O' or label_kind == 'O' def index(self, key): r""" @@ -853,7 +826,7 @@ def index(self, key): except (KeyError, TypeError, IndexError): pass - if isinstance(key, basestring): + if isinstance(key, str): # try the key as-is to allow getting at ticks with special characters (",", ":", ...) try: # avoid matching 0 against False or 0.0, note that Group keys have object dtype and so always pass this @@ -868,11 +841,11 @@ def index(self, key): # transform "specially formatted strings" for slices, lists, LGroup and IGroup to actual objects key = _to_key(key) - if not PY2 and isinstance(key, range): + if isinstance(key, range): key = list(key) # this can happen when key was passed as a string and converted to a Group via _to_key - if isinstance(key, Group) and isinstance(key.axis, basestring) and key.axis != self.name: + if isinstance(key, Group) and isinstance(key.axis, str) and key.axis != self.name: raise KeyError(key) if isinstance(key, IGroup): @@ -1168,7 +1141,7 @@ def union(self, other): >>> a.union(['a1', 'a2', 'a3']) Axis(['a0', 'a1', 'a2', 'a3'], 'a') """ - if isinstance(other, basestring): + if isinstance(other, str): # TODO : remove [other] if ... when FuturWarning raised in Axis.init will be removed other = _to_ticks(other, parse_single_int=True) if '..' in other or ',' in other else [other] if isinstance(other, Axis): @@ -1204,7 +1177,7 @@ def intersection(self, other): >>> a.intersection(['a1', 'a2', 'a3']) Axis(['a1', 'a2'], 'a') """ - if isinstance(other, basestring): + if isinstance(other, str): # TODO : remove [other] if ... when FuturWarning raised in Axis.init will be removed other = _to_ticks(other, parse_single_int=True) if '..' in other or ',' in other else [other] if isinstance(other, Axis): @@ -1241,7 +1214,7 @@ def difference(self, other): >>> a.difference(['a1', 'a2', 'a3']) Axis(['a0'], 'a') """ - if isinstance(other, basestring): + if isinstance(other, str): # TODO : remove [other] if ... when FuturWarning raised in Axis.init will be removed other = _to_ticks(other, parse_single_int=True) if '..' in other or ',' in other else [other] if isinstance(other, Axis): @@ -1441,7 +1414,7 @@ class AxisCollection(object): def __init__(self, axes=None): if axes is None: axes = [] - elif isinstance(axes, (int, long, Group, Axis)): + elif isinstance(axes, (int, Group, Axis)): axes = [axes] elif isinstance(axes, str): axes = [axis.strip() for axis in axes.split(';')] @@ -1600,7 +1573,7 @@ def __getitem__(self, key): elif key is None: raise KeyError("axis '%s' not found in %s" % (key, self)) else: - assert isinstance(key, basestring), type(key) + assert isinstance(key, str), type(key) if key in self._map: return self._map[key] else: @@ -1792,7 +1765,7 @@ def isaxis(self, value): # we could also provide an explicit kwarg (ie this would effectively forbid having an axis named "axis"). # arr.sum(axis=0). I think this is the sanest option. The error message in case we use it without the # keyword needs to be clearer though. - return isinstance(value, Axis) or (isinstance(value, basestring) and value in self) + return isinstance(value, Axis) or (isinstance(value, str) and value in self) # 2) slightly inconsistent API: allow aggregate over single labels if they are string, but not int # arr.sum(0) would sum on the first axis, but arr.sum('M') would # sum a single tick. I don't like this option. @@ -2303,7 +2276,7 @@ def replace(self, axes_to_replace=None, new_axis=None, inplace=False, **kwargs): Axis(['c0', 'c1', 'c2'], 'column') ]) """ - if not PY2 and isinstance(axes_to_replace, zip): + if isinstance(axes_to_replace, zip): axes_to_replace = list(axes_to_replace) if isinstance(axes_to_replace, (list, AxisCollection)) and \ @@ -2318,7 +2291,7 @@ def replace(self, axes_to_replace=None, new_axis=None, inplace=False, **kwargs): elif isinstance(axes_to_replace, list): assert all([isinstance(item, tuple) and len(item) == 2 for item in axes_to_replace]) items = axes_to_replace[:] - elif isinstance(axes_to_replace, (basestring, Axis, int)): + elif isinstance(axes_to_replace, (str, Axis, int)): items = [(axes_to_replace, new_axis)] else: items = [] @@ -2469,7 +2442,7 @@ def set_labels(self, axis=None, labels=None, inplace=False, **kwargs): changes = {} elif isinstance(axis, dict): changes = axis - elif isinstance(axis, (basestring, Axis, int)): + elif isinstance(axis, (str, Axis, int)): changes = {axis: labels} else: raise ValueError("Expected None or a string/int/Axis/dict instance for axis argument") @@ -2542,7 +2515,7 @@ def __sub__(self, axes): -------- without """ - if isinstance(axes, basestring): + if isinstance(axes, str): axes = axes.split(',') elif isinstance(axes, (int, Axis)): axes = [axes] @@ -2648,7 +2621,7 @@ def _translate_axis_key(self, axis_key): # Need to convert string keys to groups otherwise command like # >>> ndtest((5, 5)).drop('1[a0]') # will work although it shouldn't - if isinstance(axis_key, basestring): + if isinstance(axis_key, str): key = _to_key(axis_key) if isinstance(key, Group): axis_key = key @@ -2830,7 +2803,7 @@ def _key_to_raw_and_axes(self, key, collapse_slices=False, translate_key=True): # transform non-Array advanced keys (list and ndarray) to Array def to_la_ikey(axis, axis_key): - if isinstance(axis_key, (int, long, np.integer, slice, Array)): + if isinstance(axis_key, (int, np.integer, slice, Array)): return axis_key else: assert isinstance(axis_key, (list, np.ndarray)) @@ -3307,10 +3280,10 @@ def split_axes(self, axes=None, sep='_', names=None, regex=None): """ if axes is None: axes = {axis: None for axis in self if sep in axis.name} - elif isinstance(axes, (int, basestring, Axis)): + elif isinstance(axes, (int, str, Axis)): axes = {axes: None} elif isinstance(axes, (list, tuple)): - if all(isinstance(axis, (int, basestring, Axis)) for axis in axes): + if all(isinstance(axis, (int, str, Axis)) for axis in axes): axes = {axis: None for axis in axes} else: raise ValueError("Expected tuple or list of int, string or Axis instances") diff --git a/larray/core/constants.py b/larray/core/constants.py index 503865f48..d41f80b6d 100644 --- a/larray/core/constants.py +++ b/larray/core/constants.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, division, print_function - import numpy as np diff --git a/larray/core/expr.py b/larray/core/expr.py index 07c55e7e4..bdc48bcd3 100644 --- a/larray/core/expr.py +++ b/larray/core/expr.py @@ -1,6 +1,3 @@ -# -*- coding: utf8 -*- -from __future__ import absolute_import, division, print_function - import sys diff --git a/larray/core/group.py b/larray/core/group.py index e3134036e..d7cdcada7 100644 --- a/larray/core/group.py +++ b/larray/core/group.py @@ -1,6 +1,3 @@ -# -*- coding: utf8 -*- -from __future__ import absolute_import, division, print_function - import fnmatch import re import sys @@ -13,7 +10,6 @@ from larray.core.abstractbases import ABCAxis, ABCAxisReference, ABCArray from larray.util.oset import * from larray.util.misc import (unique, find_closing_chr, _parse_bound, _seq_summary, _isintstring, renamed_to, LHDFStore) -from larray.util.compat import basestring, PY2 def _slice_to_str(key, repr_func=str): @@ -239,7 +235,7 @@ def _range_str_to_range(s, stack_depth=1): if stop is None: raise ValueError("no stop bound provided in range: %r" % s) stop = _parse_bound(stop, stack_depth + 1) - if isinstance(start, basestring) or isinstance(stop, basestring): + if isinstance(start, str) or isinstance(stop, str): start, stop = str(start), str(stop) # TODO: use parse_bound step = int(step) if step is not None else 1 @@ -372,7 +368,6 @@ def _to_tick(v): return str(v) -# TODO: remove the conversion to list in doctests once Python 2 is dropped def _to_ticks(s, parse_single_int=False): r""" Makes a (list of) value(s) usable as the collection of labels for an Axis (ie hashable). @@ -390,18 +385,18 @@ def _to_ticks(s, parse_single_int=False): Examples -------- - >>> list(_to_ticks('M , F')) - ['M', 'F'] - >>> list(_to_ticks('A,C..E,F..G,Z')) - ['A', 'C', 'D', 'E', 'F', 'G', 'Z'] - >>> list(_to_ticks('U')) - ['U'] - >>> list(_to_ticks('..3')) - [0, 1, 2, 3] - >>> list(_to_ticks('01..12')) - ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'] - >>> list(_to_ticks('01,02,03,10,11,12')) - ['01', '02', '03', '10', '11', '12'] + >>> _to_ticks('M , F') # doctest: +NORMALIZE_WHITESPACE + array(['M', 'F'], dtype='>> _to_ticks('A,C..E,F..G,Z') # doctest: +NORMALIZE_WHITESPACE + array(['A', 'C', 'D', 'E', 'F', 'G', 'Z'], dtype='>> _to_ticks('U') # doctest: +NORMALIZE_WHITESPACE + array(['U'], dtype='>> _to_ticks('..3') # doctest: +NORMALIZE_WHITESPACE + array([0, 1, 2, 3]) + >>> _to_ticks('01..12') # doctest: +NORMALIZE_WHITESPACE + array(['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'], dtype='>> _to_ticks('01,02,03,10,11,12') # doctest: +NORMALIZE_WHITESPACE + array(['01', '02', '03', '10', '11', '12'], dtype='= '3' and isinstance(s, range): ticks = s - elif isinstance(s, basestring): + elif isinstance(s, str): seq = _seq_str_to_seq(s, parse_single_int=parse_single_int) if isinstance(seq, slice): raise ValueError("using : to define axes is deprecated, please use .. instead") - ticks = [seq] if isinstance(seq, (basestring, int)) else seq + ticks = [seq] if isinstance(seq, (str, int)) else seq elif hasattr(s, '__array__'): ticks = s.__array__() else: @@ -529,30 +524,28 @@ def _to_key(v, stack_depth=1, parse_single_int=False): 0['a0'] >>> _to_key('0.i[a0]') 0.i['a0'] - - # evaluated variables do not work on Python 2, probably because the stackdepth is different - # >>> ext = [1, 2, 3] - # >>> _to_key('{ext} >> ext') - # LGroup([1, 2, 3]) >> 'ext' - # >>> answer = 42 - # >>> _to_key('{answer}') - # 42 - # >>> _to_key('{answer} >> answer') - # LGroup(42) >> 'answer' - # >>> _to_key('10:{answer} >> answer') - # LGroup(slice(10, 42, None)) >> 'answer' - # >>> _to_key('4,{answer},2 >> answer') - # LGroup([4, 42, 2]) >> 'answer' - # >>> list(_to_key('40..{answer}')) - # [40, 41, 42] - # >>> _to_key('4,40..{answer},2') - # [4, 40, 41, 42, 2] - # >>> _to_key('4,40..{answer},2 >> answer') - # LGroup([4, 40, 41, 42, 2]) >> 'answer' + >>> ext = [1, 2, 3] + >>> _to_key('{ext} >> ext') + LGroup([1, 2, 3]) >> 'ext' + >>> answer = 42 + >>> _to_key('{answer}') + 42 + >>> _to_key('{answer} >> answer') + LGroup(42) >> 'answer' + >>> _to_key('10:{answer} >> answer') + LGroup(slice(10, 42, None)) >> 'answer' + >>> _to_key('4,{answer},2 >> answer') + LGroup([4, 42, 2]) >> 'answer' + >>> list(_to_key('40..{answer}')) + [40, 41, 42] + >>> _to_key('4,40..{answer},2') + [4, 40, 41, 42, 2] + >>> _to_key('4,40..{answer},2 >> answer') + LGroup([4, 40, 41, 42, 2]) >> 'answer' """ if isinstance(v, tuple): return list(v) - elif isinstance(v, basestring): + elif isinstance(v, str): # axis name m = _axis_name_pattern.match(v) _, axis, positional, key = m.groups() @@ -608,12 +601,11 @@ def _to_keys(value, stack_depth=1): >>> _to_keys('P01;P02,P03;:') ('P01', ['P02', 'P03'], slice(None, None, None)) - # evaluated variables do not work on Python 2, probably because the stack depth is different - # >>> ext = 'P03' - # >>> to_keys('P01,P02,{ext}') - # ['P01', 'P02', 'P03'] - # >>> to_keys('P01;P02;{ext}') - # ('P01', 'P02', 'P03') + >>> ext = 'P03' + >>> _to_keys('P01,P02,{ext}') + ['P01', 'P02', 'P03'] + >>> _to_keys('P01;P02;{ext}') + ('P01', 'P02', 'P03') >>> _to_keys('age[10:19] >> teens ; year.i[-1]') (age[10:19] >> 'teens', year.i[-1]) @@ -633,7 +625,7 @@ def _to_keys(value, stack_depth=1): >>> _to_keys((('P01',), ('P02',), ':')) (['P01'], ['P02'], slice(None, None, None)) """ - if isinstance(value, basestring) and ';' in value: + if isinstance(value, str) and ';' in value: value = tuple(value.split(';')) if isinstance(value, tuple): @@ -650,7 +642,7 @@ def _to_keys(value, stack_depth=1): def _translate_sheet_name(sheet_name): if isinstance(sheet_name, Group): sheet_name = str(_to_tick(sheet_name)) - if isinstance(sheet_name, basestring): + if isinstance(sheet_name, str): sheet_name = _sheet_name_pattern.sub('_', sheet_name) if len(sheet_name) > 31: raise ValueError("Sheet names cannot exceed 31 characters") @@ -740,7 +732,7 @@ def __init__(self, key, name=None, axis=None): # we do NOT assign a name automatically when missing because that makes it impossible to know whether a name # was explicitly given or not self.name = _to_tick(name) if name is not None else name - assert axis is None or isinstance(axis, (basestring, int, ABCAxis)), \ + assert axis is None or isinstance(axis, (str, int, ABCAxis)), \ "invalid axis '%s' (%s)" % (axis, type(axis).__name__) # we could check the key is valid but this can be slow and could be useless @@ -836,7 +828,7 @@ def retarget_to(self, target_axis): """ if self.axis is target_axis: return self - elif isinstance(self.axis, basestring) or isinstance(self.axis, ABCAxisReference): + elif isinstance(self.axis, str) or isinstance(self.axis, ABCAxisReference): axis_name = self.axis.name if isinstance(self.axis, ABCAxisReference) else self.axis if axis_name != target_axis.name: raise ValueError('cannot retarget a Group defined without a real axis object (e.g. using ' @@ -1020,26 +1012,9 @@ def _binop(opname): op_fullname = '__%s__' % opname # TODO: implement this in a delayed fashion for axes references - if PY2: - # workaround the fact slice objects do not have any __binop__ methods defined on Python2 (even though - # the actual operations work on them). - def opmethod(self, other): - self_value = self.eval() - other_value = other.eval() if isinstance(other, Group) else other - # this can only happen when self.axis is not an Axis instance - if isinstance(self_value, slice): - if not isinstance(other_value, slice): - # FIXME: we should raise a TypeError instead for all ops except == and != - # FIXME: we should return True for != - return False - # FIXME: we should raise a TypeError instead of doing this for all ops except comparison ops - self_value = (self_value.start, self_value.stop, self_value.step) - other_value = (other_value.start, other_value.stop, other_value.step) - return getattr(self_value, op_fullname)(other_value) - else: - def opmethod(self, other): - other_value = other.eval() if isinstance(other, Group) else other - return getattr(self.eval(), op_fullname)(other_value) + def opmethod(self, other): + other_value = other.eval() if isinstance(other, Group) else other + return getattr(self.eval(), op_fullname)(other_value) opmethod.__name__ = op_fullname return opmethod diff --git a/larray/core/metadata.py b/larray/core/metadata.py index 1fa6687d5..7331ecbbb 100644 --- a/larray/core/metadata.py +++ b/larray/core/metadata.py @@ -1,111 +1,24 @@ -# -*- coding: utf8 -*- -from __future__ import absolute_import, division, print_function - from collections import OrderedDict -from larray.util.compat import PY2, basestring - - -if PY2: - class AttributeDict(object): - def __init__(self, *args, **kwargs): - object.__setattr__(self, '__odict', OrderedDict(*args, **kwargs)) - - def __getattr__(self, key): - od = object.__getattribute__(self, '__odict') - if hasattr(od, key): - return getattr(od, key) - else: - try: - return od[key] - except KeyError: - raise AttributeError(key) - - def __setattr__(self, key, value): - od = object.__getattribute__(self, '__odict') - od[key] = value - - def __delattr__(self, key): - od = object.__getattribute__(self, '__odict') - del od[key] - - def __reduce__(self): - 'Return state information for pickling' - od = object.__getattribute__(self, '__odict') - res = list(od.__reduce__()) - res[0] = self.__class__ - return tuple(res) - - def __dir__(self): - od = object.__getattribute__(self, '__odict') - return list(set(dir(self.__class__)) | set(self.__dict__.keys()) | set(od.keys())) - - def copy(self): - od = object.__getattribute__(self, '__odict') - return self.__class__(od) - - def method_factory(name): - fullname = '__%s__' % name - odict_method = getattr(OrderedDict, fullname) - - def method(self, *args, **kwargs): - od = object.__getattribute__(self, '__odict') - return odict_method(od, *args, **kwargs) - return method - - __getitem__ = method_factory('getitem') - __setitem__ = method_factory('setitem') - __delitem__ = method_factory('delitem') - __contains__ = method_factory('contains') - - __iter__ = method_factory('iter') - __len__ = method_factory('len') - - __reversed__ = method_factory('reversed') - - __sizeof__ = method_factory('sizeof') - - def _binop(name): - fullname = '__%s__' % name - odict_method = getattr(OrderedDict, fullname) - - def opmethod(self, other): - self_od = object.__getattribute__(self, '__odict') - if not isinstance(other, AttributeDict): - return False - other_od = object.__getattribute__(other, '__odict') - return odict_method(self_od, other_od) - opmethod.__name__ = fullname - return opmethod - - __eq__ = _binop('eq') - __ne__ = _binop('ne') - __ge__ = _binop('ge') - __gt__ = _binop('gt') - __le__ = _binop('le') - __lt__ = _binop('lt') - - def __repr__(self): - return '\n'.join(['{}: {}'.format(k, v) for k, v in self.items()]) - -else: - class AttributeDict(OrderedDict): - def __getattr__(self, key): - try: - return self[key] - except KeyError: - raise AttributeError(key) - - def __setattr__(self, key, value): - self[key] = value - - def __delattr__(self, key): - del self[key] - - def __dir__(self): - return list(set(super(AttributeDict, self).__dir__()) | set(self.keys())) - - def __repr__(self): - return '\n'.join(['{}: {}'.format(k, v) for k, v in self.items()]) + + +class AttributeDict(OrderedDict): + def __getattr__(self, key): + try: + return self[key] + except KeyError: + raise AttributeError(key) + + def __setattr__(self, key, value): + self[key] = value + + def __delattr__(self, key): + del self[key] + + def __dir__(self): + return list(set(super(AttributeDict, self).__dir__()) | set(self.keys())) + + def __repr__(self): + return '\n'.join(['{}: {}'.format(k, v) for k, v in self.items()]) class Metadata(AttributeDict): @@ -120,7 +33,7 @@ class Metadata(AttributeDict): Add metadata at array initialization - >>> # Python 2 or <= 3.5 + >>> # Python 3.5- >>> arr = ndtest((3, 3), meta=[('title', 'the title'), ('author', 'John Smith')]) >>> # Python 3.6+ >>> arr = ndtest((3, 3), meta=Metadata(title='the title', author='John Smith')) # doctest: +SKIP @@ -157,7 +70,7 @@ def from_array(cls, array): def _convert_value(value): value = to_numeric([value], errors='ignore')[0] - if isinstance(value, basestring): + if isinstance(value, str): value = to_datetime(value, errors='ignore', infer_datetime_format=True) return value diff --git a/larray/core/session.py b/larray/core/session.py index 9c40f2028..f5403cf35 100644 --- a/larray/core/session.py +++ b/larray/core/session.py @@ -1,12 +1,10 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, division, print_function - import os import sys import re import fnmatch import warnings from collections import OrderedDict +from collections.abc import Iterable import numpy as np @@ -16,7 +14,6 @@ from larray.core.constants import nan from larray.core.array import Array, get_axes, ndtest, zeros, zeros_like, sequence, asarray from larray.util.misc import float_error_handler_factory, is_interactive_interpreter, renamed_to, inverseop -from larray.util.compat import basestring, Iterable from larray.inout.session import ext_default_engine, get_file_handler @@ -74,7 +71,7 @@ class Session(object): create a session with metadata - >>> # Python <= 3.5 + >>> # Python 3.5- >>> ses = Session([('arr1', arr1), ('arr2', arr2)], meta=[('title', 'my title'), ('author', 'John Smith')]) >>> ses.meta title: my title @@ -104,6 +101,24 @@ def __init__(self, *args, **kwargs): else: self.add(*args, **kwargs) + @property + def meta(self): + r"""Returns metadata of the session. + + Returns + ------- + Metadata: + Metadata of the session. + """ + return self._meta + + @meta.setter + def meta(self, meta): + if not isinstance(meta, (list, dict, OrderedDict, Metadata)): + raise TypeError("Expected list of pairs or dict or OrderedDict or Metadata object " + "instead of {}".format(type(meta).__name__)) + object.__setattr__(self, '_meta', meta if isinstance(meta, Metadata) else Metadata(meta)) + # XXX: behave like a dict and return keys instead? def __iter__(self): return iter(self.values()) @@ -312,22 +327,17 @@ def __setitem__(self, key, value): def __delitem__(self, key): del self._objects[key] - # TODO: add a a meta property when Python 2.7 will be dropped def __getattr__(self, key): - if key == 'meta': - return self._meta - elif key in self._objects: + if key in self._objects: return self._objects[key] else: raise AttributeError("'{}' object has no attribute '{}'".format(self.__class__.__name__, key)) - # TODO: implement meta.setter when Python 2.7 will be dropped def __setattr__(self, key, value): + # the condition below is needed because, unlike __getattr__, __setattr__ is called before any property + # see https://stackoverflow.com/a/15751159 if key == 'meta': - if not isinstance(value, (list, dict, OrderedDict, Metadata)): - raise TypeError("Expected list of pairs or dict or OrderedDict or Metadata object " - "instead of {}".format(type(value).__name__)) - object.__setattr__(self, '_meta', value if isinstance(value, Metadata) else Metadata(value)) + super().__setattr__(key, value) else: self._objects[key] = value @@ -1451,10 +1461,10 @@ def display(k, v, is_metadata=False): tmpl = template.get(t, "{key}: {value}") else: tmpl = template[Metadata] - if not (isinstance(tmpl, basestring) or callable(tmpl)): + if not (isinstance(tmpl, str) or callable(tmpl)): raise TypeError("Expected a string template or a function for type {}. " "Got {}".format(type(v), type(tmpl))) - if isinstance(tmpl, basestring): + if isinstance(tmpl, str): if isinstance(v, Axis): return tmpl.format(key=k, name=v.name, labels=v.labels_summary(), length=len(v)) elif isinstance(v, Group): diff --git a/larray/example.py b/larray/example.py index ca5c0ef8b..d7dc1af73 100644 --- a/larray/example.py +++ b/larray/example.py @@ -43,7 +43,7 @@ def get_example_filepath(fname): # Note that we skip doctests because they require pytables, which is only an optional dependency and its hard -# to skip doctests selectively. The tests output is also different between Python 2 and Python 3. +# to skip doctests selectively. # CHECK: We might want to use .csv files for the example data, so that it can be loaded with any optional dependency. def load_example_data(name): r"""Load arrays used in the tutorial so that all examples in it can be reproduced. diff --git a/larray/inout/common.py b/larray/inout/common.py index e05fa5c17..826009537 100644 --- a/larray/inout/common.py +++ b/larray/inout/common.py @@ -1,10 +1,7 @@ -from __future__ import absolute_import, print_function - import os from datetime import date, time, datetime from collections import OrderedDict -from larray.util.compat import bytes, unicode from larray.core.axis import Axis from larray.core.group import Group from larray.core.array import Array @@ -15,8 +12,7 @@ # only for HDF5 and pickle formats # support list, tuple and dict? -# replace unicode by str when Python 2.7 will no longer be supported -_supported_scalars_types = (int, float, bool, bytes, unicode, date, time, datetime) +_supported_scalars_types = (int, float, bool, bytes, str, date, time, datetime) _supported_types = _supported_larray_types + _supported_scalars_types _supported_typenames = {cls.__name__ for cls in _supported_types} diff --git a/larray/inout/csv.py b/larray/inout/csv.py index 767f22fe3..c2190024f 100644 --- a/larray/inout/csv.py +++ b/larray/inout/csv.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, print_function - import os import csv import warnings @@ -13,7 +11,6 @@ from larray.core.constants import nan from larray.core.metadata import Metadata from larray.util.misc import skip_comment_cells, strip_rows, deprecate_kwarg -from larray.util.compat import csv_open from larray.inout.session import register_file_handler from larray.inout.common import _get_index_col, FileHandler from larray.inout.pandas import df_asarray @@ -193,7 +190,7 @@ def read_csv(filepath_or_buffer, nb_axes=None, index_col=None, sep=',', headerse # read axes names. This needs to be done separately instead of reading the whole file with Pandas then # manipulating the dataframe because the header line must be ignored for the column types to be inferred # correctly. Note that to read one line, this is faster than using Pandas reader. - with csv_open(filepath_or_buffer) as f: + with open(filepath_or_buffer, mode='r', newline='', encoding='utf8') as f: reader = csv.reader(f, delimiter=sep) line_stream = skip_comment_cells(strip_rows(reader)) axes_names = next(line_stream) diff --git a/larray/inout/excel.py b/larray/inout/excel.py index 54b30c6a6..4d0f150c6 100644 --- a/larray/inout/excel.py +++ b/larray/inout/excel.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, print_function - import os import warnings from collections import OrderedDict diff --git a/larray/inout/hdf.py b/larray/inout/hdf.py index b2c64315a..a3a3cadb3 100644 --- a/larray/inout/hdf.py +++ b/larray/inout/hdf.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, print_function - import warnings import numpy as np diff --git a/larray/inout/misc.py b/larray/inout/misc.py index e84d0e320..64b7be556 100644 --- a/larray/inout/misc.py +++ b/larray/inout/misc.py @@ -1,10 +1,9 @@ -from __future__ import absolute_import, print_function +from io import StringIO from pandas import DataFrame from larray.core.constants import nan from larray.util.misc import deprecate_kwarg -from larray.util.compat import StringIO from larray.inout.common import _get_index_col from larray.inout.pandas import df_asarray, set_dataframe_index_by_position from larray.inout.csv import read_csv diff --git a/larray/inout/pandas.py b/larray/inout/pandas.py index f8680a179..327fb8606 100644 --- a/larray/inout/pandas.py +++ b/larray/inout/pandas.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, print_function - from itertools import product import numpy as np @@ -9,7 +7,14 @@ from larray.core.axis import Axis, AxisCollection from larray.core.constants import nan from larray.util.misc import unique -from larray.util.compat import basestring, decode, bytes + + +def decode(s, encoding='utf-8', errors='strict'): + if isinstance(s, bytes): + return s.decode(encoding, errors) + else: + assert s is None or isinstance(s, str), "unexpected " + str(type(s)) + return s def parse(s): @@ -17,7 +22,7 @@ def parse(s): Used to parse the "folded" axis ticks (usually periods). """ # parameters can be strings or numbers - if isinstance(s, basestring): + if isinstance(s, str): s = s.strip() low = s.lower() if low == 'true': @@ -223,7 +228,7 @@ def from_frame(df, sort_rows=False, sort_columns=False, parse_header=False, unfo # handle 2 or more dimensions with the last axis name given using \ if unfold_last_axis_name: - if isinstance(axes_names[-1], basestring) and '\\' in axes_names[-1]: + if isinstance(axes_names[-1], str) and '\\' in axes_names[-1]: last_axes = [name.strip() for name in axes_names[-1].split('\\')] axes_names = axes_names[:-1] + last_axes else: @@ -317,7 +322,7 @@ def df_asarray(df, sort_rows=False, sort_columns=False, raw=False, parse_header= if wide: try: # take the first column which contains '\' - pos_last = next(i for i, v in enumerate(columns) if isinstance(v, basestring) and '\\' in v) + pos_last = next(i for i, v in enumerate(columns) if isinstance(v, str) and '\\' in v) except StopIteration: # we assume first column will not contain data pos_last = 0 @@ -333,7 +338,7 @@ def df_asarray(df, sort_rows=False, sort_columns=False, raw=False, parse_header= # handle 1D arrays if len(df) == 1 and (pd.isnull(df.index.values[0]) or - (isinstance(df.index.values[0], basestring) and df.index.values[0].strip() == '')): + (isinstance(df.index.values[0], str) and df.index.values[0].strip() == '')): if parse_header: df.columns = pd.Index([parse(cell) for cell in df.columns.values], name=df.columns.name) series = df.iloc[0] @@ -349,7 +354,7 @@ def parse_axis_name(name): name = None return name axes_names = [parse_axis_name(name) for name in df.index.names] - unfold_last_axis_name = isinstance(axes_names[-1], basestring) and '\\' in axes_names[-1] + unfold_last_axis_name = isinstance(axes_names[-1], str) and '\\' in axes_names[-1] res = from_frame(df, sort_rows=sort_rows, sort_columns=sort_columns, parse_header=parse_header, unfold_last_axis_name=unfold_last_axis_name, cartesian_prod=cartesian_prod, **kwargs) @@ -358,6 +363,6 @@ def parse_axis_name(name): # make them roundtrip correctly, based on the assumption that in an in-memory LArray an anonymouse axis is more # likely and useful than an Axis with an empty name. # TODO : find a more robust and elegant solution - res = res.rename({axis: None for axis in res.axes if isinstance(axis.name, basestring) and + res = res.rename({axis: None for axis in res.axes if isinstance(axis.name, str) and (axis.name == '' or 'Unnamed:' in axis.name)}) return res diff --git a/larray/inout/pickle.py b/larray/inout/pickle.py index 3f39c72a3..6bfa96d5e 100644 --- a/larray/inout/pickle.py +++ b/larray/inout/pickle.py @@ -1,5 +1,4 @@ -from __future__ import absolute_import, division, print_function - +import pickle import os.path from collections import OrderedDict @@ -7,7 +6,6 @@ from larray.core.group import Group from larray.core.array import Array from larray.core.metadata import Metadata -from larray.util.compat import pickle from larray.inout.session import register_file_handler from larray.inout.common import FileHandler, _supported_types, _supported_typenames, _supported_scalars_types diff --git a/larray/inout/sas.py b/larray/inout/sas.py index aed18aff9..f9fbf1263 100644 --- a/larray/inout/sas.py +++ b/larray/inout/sas.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, print_function - import warnings import numpy as np diff --git a/larray/inout/session.py b/larray/inout/session.py index c10f42b70..b587e1bf1 100644 --- a/larray/inout/session.py +++ b/larray/inout/session.py @@ -1,7 +1,3 @@ -from __future__ import absolute_import, division, print_function - -from larray.util.compat import basestring - handler_classes = {} ext_default_engine = {} @@ -22,7 +18,7 @@ def decorate_class(cls): handler_classes[engine] = cls if extensions is None: exts = [] - elif isinstance(extensions, basestring): + elif isinstance(extensions, str): exts = [extensions] else: exts = extensions diff --git a/larray/inout/stata.py b/larray/inout/stata.py index 5741e4525..79473e05f 100644 --- a/larray/inout/stata.py +++ b/larray/inout/stata.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, print_function - import pandas as pd from larray.inout.pandas import from_frame diff --git a/larray/inout/xw_excel.py b/larray/inout/xw_excel.py index d7bd61fbc..345b8a8c4 100644 --- a/larray/inout/xw_excel.py +++ b/larray/inout/xw_excel.py @@ -1,6 +1,3 @@ -# -*- coding: utf8 -*- -from __future__ import absolute_import, print_function - import os import atexit @@ -17,7 +14,6 @@ from larray.inout.pandas import df_asarray from larray.inout.misc import from_lists from larray.util.misc import deprecate_kwarg -from larray.util.compat import PY2 string_types = (str,) @@ -649,8 +645,7 @@ def open_excel(filepath=None, overwrite_file=False, visible=None, silent=None, a # We define Workbook and open_excel documentation here since Readthedocs runs on Linux -if not PY2: - Workbook.__doc__ = r""" +Workbook.__doc__ = r""" Excel Workbook. See Also @@ -658,7 +653,7 @@ def open_excel(filepath=None, overwrite_file=False, visible=None, silent=None, a open_excel """ - Workbook.sheet_names.__doc__ = r""" +Workbook.sheet_names.__doc__ = r""" Returns the names of the Excel sheets. Examples @@ -674,7 +669,7 @@ def open_excel(filepath=None, overwrite_file=False, visible=None, silent=None, a ['arr', 'arr2', 'arr3'] """ - Workbook.save.__doc__ = r""" +Workbook.save.__doc__ = r""" Saves the Workbook. If a path is being provided, this works like SaveAs() in Excel. @@ -697,7 +692,7 @@ def open_excel(filepath=None, overwrite_file=False, visible=None, silent=None, a ... wb.save() """ - Workbook.close.__doc__ = r""" +Workbook.close.__doc__ = r""" Close the workbook in Excel. Need to be called if the workbook has been opened without the `with` statement. @@ -713,7 +708,7 @@ def open_excel(filepath=None, overwrite_file=False, visible=None, silent=None, a >>> wb.close() # doctest: +SKIP """ - Workbook.app.__doc__ = r""" +Workbook.app.__doc__ = r""" Return the Excel instance this workbook is attached to. """ diff --git a/larray/inout/xw_reporting.py b/larray/inout/xw_reporting.py index fb7ae231a..9f0929641 100644 --- a/larray/inout/xw_reporting.py +++ b/larray/inout/xw_reporting.py @@ -3,7 +3,6 @@ from collections import OrderedDict from larray.util.misc import _positive_integer, _validate_dir -from larray.util.compat import PY2 from larray.core.group import _translate_sheet_name from larray.core.array import asarray, zip_array_items from larray.example import load_example_data, EXAMPLE_EXCEL_TEMPLATES_DIR @@ -766,6 +765,5 @@ def __init__(self): raise Exception("ExcelReport class cannot be instantiated because xlwings is not installed") -if not PY2: - ExcelReport.__doc__ = AbstractExcelReport.__doc__ - ReportSheet.__doc__ = AbstractReportSheet.__doc__ +ExcelReport.__doc__ = AbstractExcelReport.__doc__ +ReportSheet.__doc__ = AbstractReportSheet.__doc__ diff --git a/larray/ipfp/__init__.py b/larray/ipfp/__init__.py index 953093522..c2138a17e 100644 --- a/larray/ipfp/__init__.py +++ b/larray/ipfp/__init__.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - import warnings import larray.extra.ipfp as ipfp diff --git a/larray/tests/common.py b/larray/tests/common.py index 2b3655a4f..3412a0d2b 100644 --- a/larray/tests/common.py +++ b/larray/tests/common.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, division, print_function - import os import sys diff --git a/larray/tests/test_array.py b/larray/tests/test_array.py index 8b6f41fc9..0a1426311 100644 --- a/larray/tests/test_array.py +++ b/larray/tests/test_array.py @@ -1,6 +1,3 @@ -# -*- coding: utf8 -*- -from __future__ import absolute_import, division, print_function - import os import re import sys @@ -8,6 +5,8 @@ import pytest import numpy as np import pandas as pd + +from io import StringIO from collections import OrderedDict from larray.tests.common import (inputpath, tmp_path, meta, @@ -20,7 +19,6 @@ from larray.inout.pandas import from_series from larray.core.axis import _to_ticks, _to_key from larray.util.misc import LHDFStore -from larray.util.compat import StringIO from larray.core.metadata import Metadata @@ -2553,10 +2551,9 @@ def test_transpose(): res = arr.transpose('b') assert res.axes == [b, a, c] - # using Ellipsis instead of ... to avoid a syntax error on Python 2 (where ... is only available within []) - res = arr.transpose(Ellipsis, 'a') + res = arr.transpose(..., 'a') assert res.axes == [b, c, a] - res = arr.transpose('c', Ellipsis, 'a') + res = arr.transpose('c', ..., 'a') assert res.axes == [c, b, a] @@ -4566,29 +4563,27 @@ def test_matmul(): a1 = ndtest([Axis(3), Axis(3)]) a2 = eye(3, 3) * 2 - # Note that we cannot use @ because that is an invalid syntax in Python 2 - # Array value - assert_array_equal(a1.__matmul__(a2), ndtest([Axis(3), Axis(3)]) * 2) + assert_array_equal(a1 @ a2, ndtest([Axis(3), Axis(3)]) * 2) # ndarray value - assert_array_equal(a1.__matmul__(a2.data), ndtest([Axis(3), Axis(3)]) * 2) + assert_array_equal(a1 @ a2.data, ndtest([Axis(3), Axis(3)]) * 2) # non anonymous axes (N <= 2) arr1d = ndtest(3) arr2d = ndtest((3, 3)) # 1D @ 1D - res = arr1d.__matmul__(arr1d) + res = arr1d @ arr1d assert isinstance(res, np.integer) assert res == 5 # 1D @ 2D - assert_array_equal(arr1d.__matmul__(arr2d), + assert_array_equal(arr1d @ arr2d, Array([15, 18, 21], 'b=b0..b2')) # 2D @ 1D - assert_array_equal(arr2d.__matmul__(arr1d), + assert_array_equal(arr2d @ arr1d, Array([5, 14, 23], 'a=a0..a2')) # 2D(a,b) @ 2D(a,b) -> 2D(a,b) @@ -4596,20 +4591,18 @@ def test_matmul(): ['a0', 15, 18, 21], ['a1', 42, 54, 66], ['a2', 69, 90, 111]]) - assert_array_equal(arr2d.__matmul__(arr2d), res) + assert_array_equal(arr2d @ arr2d, res) # 2D(a,b) @ 2D(b,a) -> 2D(a,a) res = from_lists([['a\\a', 'a0', 'a1', 'a2'], ['a0', 5, 14, 23], ['a1', 14, 50, 86], ['a2', 23, 86, 149]]) - assert_array_equal(arr2d.__matmul__(arr2d.T), res) + assert_array_equal(arr2d @ arr2d.T, res) # ndarray value - assert_array_equal(arr1d.__matmul__(arr2d.data), - Array([15, 18, 21])) - assert_array_equal(arr2d.data.__matmul__(arr2d.T.data), - res.data) + assert_array_equal(arr1d @ arr2d.data, Array([15, 18, 21])) + assert_array_equal(arr2d.data @ arr2d.T.data, res.data) # different axes a1 = ndtest('a=a0..a1;b=b0..b2') @@ -4617,7 +4610,7 @@ def test_matmul(): res = from_lists([[r'a\c', 'c0', 'c1', 'c2', 'c3'], ['a0', 20, 23, 26, 29], ['a1', 56, 68, 80, 92]]) - assert_array_equal(a1.__matmul__(a2), res) + assert_array_equal(a1 @ a2, res) # non anonymous axes (N >= 2) arr2d = ndtest((2, 2)) @@ -4646,7 +4639,7 @@ def test_matmul(): ['a1', 'b1', 'e0', 'c1', 30, 59], ['a1', 'b1', 'e1', 'c0', 126, 151], ['a1', 'b1', 'e1', 'c1', 146, 175]]) - assert_array_equal(arr4d.__matmul__(arr3d), res) + assert_array_equal(arr4d @ arr3d, res) # 3D(e, d, f) @ 4D(a, b, c, d) -> 5D(e, a, b, d, d) res = from_lists([['e', 'a', 'b', 'd\\d', 'd0', 'd1'], @@ -4666,7 +4659,7 @@ def test_matmul(): ['e1', 'a1', 'b0', 'd1', 118, 131], ['e1', 'a1', 'b1', 'd0', 118, 127], ['e1', 'a1', 'b1', 'd1', 170, 183]]) - assert_array_equal(arr3d.__matmul__(arr4d), res) + assert_array_equal(arr3d @ arr4d, res) # 4D(a, b, c, d) @ 3D(b, d, f) -> 4D(a, b, c, f) arr3d = arr3d.set_axes([b, d, f]) @@ -4679,7 +4672,7 @@ def test_matmul(): ['a1', 'b0', 'c1', 22, 43], ['a1', 'b1', 'c0', 126, 151], ['a1', 'b1', 'c1', 146, 175]]) - assert_array_equal(arr4d.__matmul__(arr3d), res) + assert_array_equal(arr4d @ arr3d, res) # 3D(b, d, f) @ 4D(a, b, c, d) -> 4D(b, a, d, d) res = from_lists([['b', 'a', 'd\\d', 'd0', 'd1'], @@ -4691,7 +4684,7 @@ def test_matmul(): ['b1', 'a0', 'd1', 66, 79], ['b1', 'a1', 'd0', 118, 127], ['b1', 'a1', 'd1', 170, 183]]) - assert_array_equal(arr3d.__matmul__(arr4d), res) + assert_array_equal(arr3d @ arr4d, res) # 4D(a, b, c, d) @ 2D(d, f) -> 5D(a, b, c, f) arr2d = arr2d.set_axes([d, f]) @@ -4704,7 +4697,7 @@ def test_matmul(): ['a1', 'b0', 'c1', 22, 43], ['a1', 'b1', 'c0', 26, 51], ['a1', 'b1', 'c1', 30, 59]]) - assert_array_equal(arr4d.__matmul__(arr2d), res) + assert_array_equal(arr4d @ arr2d, res) # 2D(d, f) @ 4D(a, b, c, d) -> 5D(a, b, d, d) res = from_lists([['a', 'b', 'd\\d', 'd0', 'd1'], @@ -4716,7 +4709,7 @@ def test_matmul(): ['a1', 'b0', 'd1', 46, 51], ['a1', 'b1', 'd0', 14, 15], ['a1', 'b1', 'd1', 66, 71]]) - assert_array_equal(arr2d.__matmul__(arr4d), res) + assert_array_equal(arr2d @ arr4d, res) @needs_python35 diff --git a/larray/tests/test_axis.py b/larray/tests/test_axis.py index fd9bb4bbd..059b60bf0 100644 --- a/larray/tests/test_axis.py +++ b/larray/tests/test_axis.py @@ -1,6 +1,3 @@ -# -*- coding: utf8 -*- -from __future__ import absolute_import, division, print_function - import pytest import os.path import numpy as np diff --git a/larray/tests/test_axiscollection.py b/larray/tests/test_axiscollection.py index 4846114e8..ca89f1784 100644 --- a/larray/tests/test_axiscollection.py +++ b/larray/tests/test_axiscollection.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, division, print_function - import numpy as np import pytest diff --git a/larray/tests/test_example.py b/larray/tests/test_example.py index c7107492e..037a0808c 100644 --- a/larray/tests/test_example.py +++ b/larray/tests/test_example.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, division, print_function - import pytest from larray.tests.common import needs_pytables diff --git a/larray/tests/test_excel.py b/larray/tests/test_excel.py index a3ef75d3c..ca2b42e93 100644 --- a/larray/tests/test_excel.py +++ b/larray/tests/test_excel.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, division, print_function - import re import os diff --git a/larray/tests/test_group.py b/larray/tests/test_group.py index a75f99f9b..a6c974cf0 100644 --- a/larray/tests/test_group.py +++ b/larray/tests/test_group.py @@ -1,6 +1,3 @@ -# -*- coding: utf8 -*- -from __future__ import absolute_import, division, print_function - import pytest import os.path import numpy as np diff --git a/larray/tests/test_ipfp.py b/larray/tests/test_ipfp.py index 2eeb75990..d40a9e461 100644 --- a/larray/tests/test_ipfp.py +++ b/larray/tests/test_ipfp.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, division, print_function - import pytest from larray.tests.common import assert_array_equal from larray import Axis, Array, ndtest, ipfp, X diff --git a/larray/tests/test_options.py b/larray/tests/test_options.py index c37d21633..b3ded4eaf 100644 --- a/larray/tests/test_options.py +++ b/larray/tests/test_options.py @@ -1,4 +1,3 @@ -from __future__ import absolute_import, division, print_function import pytest import larray diff --git a/larray/tests/test_session.py b/larray/tests/test_session.py index 9d9f22410..39590b127 100644 --- a/larray/tests/test_session.py +++ b/larray/tests/test_session.py @@ -1,7 +1,6 @@ -from __future__ import absolute_import, division, print_function - import os import shutil +import pickle from datetime import date, time, datetime import numpy as np @@ -13,7 +12,6 @@ from larray.inout.common import _supported_scalars_types from larray import (Session, Axis, Array, Group, isnan, zeros_like, ndtest, ones_like, ones, full, local_arrays, global_arrays, arrays) -from larray.util.compat import pickle, PY2 def equal(o1, o2): @@ -192,7 +190,7 @@ def _test_io(fpath, session, meta, engine): # use Session.names instead of Session.keys because CSV, Excel and HDF do *not* keep ordering assert s.names == session.names assert s.equals(session) - if not PY2 and not is_excel_or_csv: + if not is_excel_or_csv: for key in s.filter(kind=Axis).keys(): assert s[key].dtype == session[key].dtype if engine != 'pandas_excel': diff --git a/larray/util/compat.py b/larray/util/compat.py deleted file mode 100644 index 1244dfba6..000000000 --- a/larray/util/compat.py +++ /dev/null @@ -1,58 +0,0 @@ -import sys - -try: - # the abstract base classes were moved to the abc sub-module in Python 3.3 but there is a backward compatibility - # layer for Python up to 3.7 - from collections.abc import Iterable, Sequence -except ImportError: - # needed for Python < 3.3 (including 2.7) - from collections import Iterable, Sequence - -try: - import builtins -except ImportError: - import __builtin__ as builtins - -try: - from itertools import izip -except ImportError: - izip = zip - -if sys.version_info[0] < 3: - basestring = basestring - bytes = str - unicode = unicode - long = long - PY2 = True -else: - basestring = str - bytes = bytes - unicode = str - long = int - PY2 = False - -if PY2: - from StringIO import StringIO -else: - from io import StringIO - -if PY2: - import cPickle as pickle -else: - import pickle - - -def csv_open(filename, mode='r'): - assert 'b' not in mode and 't' not in mode - if PY2: - return open(filename, mode + 'b') - else: - return open(filename, mode, newline='', encoding='utf8') - - -def decode(s, encoding='utf-8', errors='strict'): - if isinstance(s, bytes): - return s.decode(encoding, errors) - else: - assert s is None or isinstance(s, unicode), "unexpected " + str(type(s)) - return s diff --git a/larray/util/misc.py b/larray/util/misc.py index 95f9ae5f7..d1ca4ea0e 100644 --- a/larray/util/misc.py +++ b/larray/util/misc.py @@ -1,7 +1,6 @@ """ Misc tools """ -from __future__ import absolute_import, division, print_function import __main__ import math @@ -18,8 +17,6 @@ import numpy as np import pandas as pd -from larray.util.compat import PY2 - try: np.set_printoptions(legacy='1.13') except TypeError: @@ -799,7 +796,7 @@ def __getitem__(self, key): return SequenceZip([seq[key] for seq in self.sequences]) def __iter__(self): - return iter(zip(*self.sequences)) if PY2 else zip(*self.sequences) + return zip(*self.sequences) def __repr__(self): return 'SequenceZip({})'.format(self.sequences) diff --git a/larray/util/options.py b/larray/util/options.py index ed50e5713..1af7ee696 100644 --- a/larray/util/options.py +++ b/larray/util/options.py @@ -1,4 +1,3 @@ -from __future__ import absolute_import, division, print_function from larray.util.misc import _positive_integer DISPLAY_PRECISION = 'display_precision' diff --git a/larray/viewer/__init__.py b/larray/viewer/__init__.py index 346e08e0e..7a3168366 100644 --- a/larray/viewer/__init__.py +++ b/larray/viewer/__init__.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, division, print_function - def view(obj=None, title='', depth=0): r""" diff --git a/setup.py b/setup.py index db97a1522..360c8c113 100644 --- a/setup.py +++ b/setup.py @@ -30,8 +30,6 @@ def readlocal(fname): 'Intended Audience :: Science/Research', 'Intended Audience :: Developers', 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', From a36168a746728b2c190c199eb3a026b4ef35691b Mon Sep 17 00:00:00 2001 From: Alix Damman Date: Thu, 20 Feb 2020 09:49:30 +0100 Subject: [PATCH 2/2] cleanup: - removed remaining references to Python 3.5 (which is no longer supported) - removed needs_python35 and needs_python36 decorators --- condarecipe/larray/conda_build_config.yaml | 1 - larray/core/metadata.py | 5 +---- larray/core/session.py | 12 +++--------- larray/tests/common.py | 2 -- larray/tests/test_array.py | 5 +---- setup.py | 1 - 6 files changed, 5 insertions(+), 21 deletions(-) diff --git a/condarecipe/larray/conda_build_config.yaml b/condarecipe/larray/conda_build_config.yaml index c3ae13fbe..a328eb668 100644 --- a/condarecipe/larray/conda_build_config.yaml +++ b/condarecipe/larray/conda_build_config.yaml @@ -1,4 +1,3 @@ python: - - 3.5 - 3.6 - 3.7 diff --git a/larray/core/metadata.py b/larray/core/metadata.py index 7331ecbbb..9f1531bb1 100644 --- a/larray/core/metadata.py +++ b/larray/core/metadata.py @@ -33,10 +33,7 @@ class Metadata(AttributeDict): Add metadata at array initialization - >>> # Python 3.5- - >>> arr = ndtest((3, 3), meta=[('title', 'the title'), ('author', 'John Smith')]) - >>> # Python 3.6+ - >>> arr = ndtest((3, 3), meta=Metadata(title='the title', author='John Smith')) # doctest: +SKIP + >>> arr = ndtest((3, 3), meta=Metadata(title='the title', author='John Smith')) Add metadata after array initialization diff --git a/larray/core/session.py b/larray/core/session.py index f5403cf35..1b395b1f4 100644 --- a/larray/core/session.py +++ b/larray/core/session.py @@ -57,11 +57,11 @@ class Session(object): >>> ses = Session([('i', i), ('s', s), ('a', a), ('b', b), ('a01', a01), ... ('arr1', arr1), ('arr2', arr2)]) - create a Session using keyword arguments (but you lose order on Python < 3.6) + create a Session using keyword arguments >>> ses = Session(i=i, s=s, a=a, b=b, a01=a01, arr1=arr1, arr2=arr2) - create a Session by passing a dictionary (but you lose order on Python < 3.6) + create a Session by passing a dictionary >>> ses = Session({'i': i, 's': s, 'a': a, 'b': b, 'a01': a01, 'arr1': arr1, 'arr2': arr2}) @@ -71,13 +71,7 @@ class Session(object): create a session with metadata - >>> # Python 3.5- - >>> ses = Session([('arr1', arr1), ('arr2', arr2)], meta=[('title', 'my title'), ('author', 'John Smith')]) - >>> ses.meta - title: my title - author: John Smith - >>> # Python 3.6+ - >>> ses = Session(arr1=arr1, arr2=arr2, meta=Metadata(title='my title', author='John Smith')) # doctest: +SKIP + >>> ses = Session(arr1=arr1, arr2=arr2, meta=Metadata(title='my title', author='John Smith')) >>> ses.meta title: my title author: John Smith diff --git a/larray/tests/common.py b/larray/tests/common.py index 3412a0d2b..4109450d8 100644 --- a/larray/tests/common.py +++ b/larray/tests/common.py @@ -148,6 +148,4 @@ def meta(): needs_xlrd = pytest.mark.skipif(xlrd is None, reason="xlrd is required for this test") needs_xlsxwriter = pytest.mark.skipif(xlsxwriter is None, reason="xlsxwriter is required for this test") -needs_python35 = pytest.mark.skipif(sys.version_info < (3, 5), reason="Python 3.5 is required for this test") -needs_python36 = pytest.mark.skipif(sys.version_info < (3, 6), reason="Python 3.6 is required for this test") needs_python37 = pytest.mark.skipif(sys.version_info < (3, 7), reason="Python 3.7 is required for this test") diff --git a/larray/tests/test_array.py b/larray/tests/test_array.py index 0a1426311..bbe17eadd 100644 --- a/larray/tests/test_array.py +++ b/larray/tests/test_array.py @@ -12,7 +12,7 @@ from larray.tests.common import (inputpath, tmp_path, meta, assert_array_equal, assert_array_nan_equal, assert_larray_equiv, assert_larray_equal, needs_xlwings, needs_pytables, needs_xlsxwriter, needs_xlrd, - needs_python35, needs_python36, needs_python37) + needs_python37) from larray import (Array, LArray, Axis, LGroup, union, zeros, zeros_like, ndtest, empty, ones, eye, diag, stack, clip, exp, where, X, mean, isnan, round, read_hdf, read_csv, read_eurostat, read_excel, from_lists, from_string, open_excel, from_frame, sequence, nan, IGroup) @@ -4557,7 +4557,6 @@ def test_diag(): assert d.i[1] == 1.0 -@needs_python35 def test_matmul(): # 2D / anonymous axes a1 = ndtest([Axis(3), Axis(3)]) @@ -4712,7 +4711,6 @@ def test_matmul(): assert_array_equal(arr2d @ arr4d, res) -@needs_python35 def test_rmatmul(): a1 = eye(3) * 2 a2 = ndtest([Axis(3), Axis(3)]) @@ -5070,7 +5068,6 @@ def test_stack(): assert_array_equal(res, expected) -@needs_python36 def test_stack_kwargs_no_axis_labels(): # these tests rely on kwargs ordering, hence python 3.6 diff --git a/setup.py b/setup.py index 360c8c113..95bf89035 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,6 @@ def readlocal(fname): 'Intended Audience :: Developers', 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Topic :: Scientific/Engineering',