Skip to content

Commit

Permalink
Merge pull request #162 from sciris/fixes-jan2021
Browse files Browse the repository at this point in the history
Release 1.0.1
  • Loading branch information
cliffckerr committed Mar 2, 2021
2 parents feb81d2 + ce48a4a commit 9f1305e
Show file tree
Hide file tree
Showing 10 changed files with 299 additions and 173 deletions.
20 changes: 20 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,26 @@ All notable changes to this project will be documented in this file.
By import convention, components of the Sciris library are listed beginning with ``sc.``, e.g. ``sc.odict()``.


Version 1.0.1 (2021-03-01)
---------------------------
1. Fixed bug with Matplotlib 3.4.0 also defining colormap ``'turbo'``, which caused Sciris to fail to load.
2. Added a new function, ``sc.orderlegend()``, that lets you specify the order you want the legend items to appear.
3. Fixed bug with paths returned by ``sc.getfilelist(nopath=True)``.
4. Fixed bug with ``sc.loadjson()`` only reading from a string if ``fromfile=False``.
5. Fixed recursion issue with printing ``sc.Failed`` objects.
6. Changed ``sc.approx()`` to be an alias to ``np.isclose()``; this function may be removed in future versions.
7. Changed ``sc.findinds()`` to call ``np.isclose()``, allowing for greater flexibility.
8. Changed the ``repr`` for ``sc.objdict()`` to differ from ``sc.odict()``.
9. Improved ``sc.maximize()`` to work on more platforms (but still not inline or on Macs).
10. Improved the flexiblity of ``sc.htmlify()`` to handle tabs and other kinds of newlines.
11. Added additional checks to ``sc.prepr()`` to avoid failing on recursive objects.
12. Updated ``sc.mergedicts()`` to return the same type as the first dict supplied.
13. Updated ``sc.readdate()`` and ``sc.date()`` to support timestamps as well as strings.
14. Updated ``sc.gitinfo()`` to try each piece independently, so if it fails on one (e.g., extracting the date) it will still return the other pieces (e.g., the hash).
15. Pinned ``xlrd`` to 1.2.0 since later versions fail to read xlsx files.



Version 1.0.0 (2020-11-30)
--------------------------
This major update (and official release!) includes many new utilities adopted from the `Covasim <http://covasim.org>`__ and `Atomica <http://atomica.tools>`__ libraries, as well as important improvements and bugfixes for parallel processing, object representation, and file I/O.
Expand Down
34 changes: 19 additions & 15 deletions sciris/sc_fileio.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ def getfilelist(folder='.', pattern=None, abspath=False, nopath=False, filesonly
nopath (bool): whether to return no path
filesonly (bool): whether to only return files (not folders)
foldersonly (bool): whether to only return folders (not files)
recursive (bool): passd to glob()
recursive (bool): passed to glob()
Returns:
List of files/folders
Expand All @@ -258,7 +258,7 @@ def getfilelist(folder='.', pattern=None, abspath=False, nopath=False, filesonly
elif foldersonly:
filelist = [f for f in filelist if os.path.isdir(f)]
if nopath:
filelist = [f[len(folder):] for f in filelist]
filelist = [os.path.basename(f) for f in filelist]
return filelist


Expand Down Expand Up @@ -520,23 +520,23 @@ def loadjson(filename=None, folder=None, string=None, fromfile=True, **kwargs):
Returns:
output (dict): the JSON object
**Example**::
**Examples**::
json = sc.loadjson('my-file.json')
json = sc.loadjson(string='{"a":null, "b":[1,2,3]}')
'''
if fromfile:
filename = makefilepath(filename=filename, folder=folder)
try:
with open(filename) as f:
output = json.load(f, **kwargs)
except FileNotFoundError as E:
print('Warning: file not found. Use fromfile=False if not loading from a file')
raise E
else:
if string is not None or not fromfile:
if string is None and filename is not None:
string = filename # Swap arguments
output = json.loads(string, **kwargs)

else:
filepath = makefilepath(filename=filename, folder=folder)
try:
with open(filepath) as f:
output = json.load(f, **kwargs)
except FileNotFoundError as E:
errormsg = f'No such file "{filename}". Use fromfile=False if loading a JSON string rather than a file.'
raise FileNotFoundError(errormsg) from E
return output


Expand Down Expand Up @@ -1093,7 +1093,7 @@ def __init__(self, *args, **kwargs):
pass

def __repr__(self):
output = ut.prepr(self) # This does not include failure_info since it's a class attribute
output = ut.objrepr(self) # This does not include failure_info since it's a class attribute
output += self.showfailures(verbose=False, tostring=True)
return output

Expand Down Expand Up @@ -1400,4 +1400,8 @@ def _unpickleMethod(im_name, im_self, im_class):
bound = types.MethodType(methodFunction, im_self, *maybeClass)
return bound

cpreg.pickle(types.MethodType, _pickleMethod, _unpickleMethod)
cpreg.pickle(types.MethodType, _pickleMethod, _unpickleMethod)

# Legacy support for loading Sciris <1.0 objects; may be removed in future
pickleMethod = _pickleMethod
unpickleMethod = _unpickleMethod
32 changes: 19 additions & 13 deletions sciris/sc_math.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,25 @@
__all__ = ['approx', 'safedivide', 'findinds', 'findfirst', 'findlast', 'findnearest', 'dataindex', 'getvalidinds', 'sanitize', 'getvaliddata', 'isprime']


def approx(val1=None, val2=None, eps=None):
def approx(val1=None, val2=None, eps=None, **kwargs):
'''
Determine whether two scalars approximately match.
Determine whether two scalars (or an array and a scalar) approximately match.
Alias for np.isclose() and may be removed in future versions.
Args:
val1 (number or array): the first value
val2 (number): the second value
eps (float): absolute tolerance
kwargs (dict): passed to np.isclose()
**Examples**::
sc.approx(2*6, 11.9999999, eps=1e-6) # Returns True
sc.approx([3,12,11.9], 12) # Returns array([False, True, False], dtype=bool)
Note: in most cases, np.isclose() is preferable.
'''
if val2 is None: val2 = 0.0
if eps is None: eps = 1e-9
if isinstance(val1, list): val1 = np.array(val1) # If it's a list, convert to an array first
output = abs(val1-val2)<=eps
if eps is not None:
kwargs['atol'] = eps # Rename kwarg to match np.isclose()
output = np.isclose(a=val1, b=val2, **kwargs)
return output


Expand Down Expand Up @@ -71,26 +75,27 @@ def safedivide(numerator=None, denominator=None, default=None, eps=None, warn=Fa
return output


def findinds(arr, val=None, eps=1e-6, first=False, last=False):
def findinds(arr, val=None, eps=1e-6, first=False, last=False, **kwargs):
'''
Little function to find matches even if two things aren't eactly equal (eg.
due to floats vs. ints). If one argument, find nonzero values. With two arguments,
check for equality using eps. Returns a tuple of arrays if val1 is multidimensional,
else returns an array.
else returns an array. Similar to calling np.nonzero(np.isclose(arr, val)).
Args:
arr (array): the array to find values in
val (float): if provided, the value to match
eps (float): the precision for matching (default 1e-6)
eps (float): the precision for matching (default 1e-6, equivalent to np.isclose's atol)
first (bool): whether to return the first matching value
last (bool): whether to return the last matching value
kwargs (dict): passed to np.isclose()
**Examples**::
sc.findinds(rand(10)<0.5) # e.g. array([2, 4, 5, 9])
sc.findinds([2,3,6,3], 6) # e.g. array([2])
Version: 2020nov29
Version: 2021mar01
'''

# Handle first or last
Expand All @@ -102,6 +107,7 @@ def findinds(arr, val=None, eps=1e-6, first=False, last=False):
ind = -1
else:
ind = None
atol = kwargs.pop('atol', eps) # Ensure atol isn't specified twice

# Calculate matches
arr = np.array(arr)
Expand All @@ -111,7 +117,7 @@ def findinds(arr, val=None, eps=1e-6, first=False, last=False):
if ut.isstring(val):
output = np.nonzero(arr==val)
else:
output = np.nonzero(abs(arr-val)<eps) # If absolute difference between the two values is less than a certain amount
output = np.nonzero(np.isclose(a=arr, b=val, atol=atol, **kwargs)) # If absolute difference between the two values is less than a certain amount

# Process output
if arr.ndim == 1: # Uni-dimensional
Expand Down
24 changes: 14 additions & 10 deletions sciris/sc_odict.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ def __setitem__(self, key, value):
return None


def __repr__(self, maxlen=None, showmultilines=True, divider=False, dividerthresh=10, numindents=0, recursionlevel=0, sigfigs=None, numformat=None, maxitems=200):
def __repr__(self, maxlen=None, showmultilines=True, divider=False, dividerthresh=10, numindents=0, recursionlevel=0, sigfigs=None, numformat=None, maxitems=200, classname='odict()', quote='"', keysep=':'):
''' Print a meaningful representation of the odict '''

# Set primitives for display.
Expand All @@ -181,9 +181,9 @@ def __repr__(self, maxlen=None, showmultilines=True, divider=False, dividerthres
halfmax = int(maxitems/2)
extraitems = 0
if nkeys == 0:
output = 'odict()'
output = classname
else:
output = '' # Initialize the output to nothing.
output = ''
keystrs = [] # Start with an empty list which we'll save key strings in.
valstrs = [] # Start with an empty list which we'll save value strings in.
vallinecounts = [] # Start with an empty list which we'll save line counts in.
Expand All @@ -202,7 +202,7 @@ def __repr__(self, maxlen=None, showmultilines=True, divider=False, dividerthres
if recursionlevel <= maxrecursion:
thisvalstr = ut.flexstr(thisval.__repr__(maxlen=maxlen, showmultilines=showmultilines, divider=divider, dividerthresh=dividerthresh, numindents=numindents, recursionlevel=recursionlevel+1, sigfigs=sigfigs, numformat=numformat))
else:
thisvalstr = 'odict() [maximum recursion reached]'
thisvalstr = f'{classname} [maximum recursion reached]'
elif ut.isnumber(thisval): # Flexibly print out numbers, since they're largely why we're here
if numformat is not None:
thisvalstr = numformat % thisval
Expand All @@ -213,7 +213,7 @@ def __repr__(self, maxlen=None, showmultilines=True, divider=False, dividerthres
else: # Otherwise, do the normal repr() read.
thisvalstr = repr(thisval)
except Exception as E:
thisvalstr = '%s read failed: %s' % (ut.objectid(thisval), str(E))
thisvalstr = f'{ut.objectid(thisval)} read failed: {str(E)}'

# Add information to the lists to retrace afterwards.
keystrs.append(thiskeystr)
Expand Down Expand Up @@ -250,9 +250,9 @@ def __repr__(self, maxlen=None, showmultilines=True, divider=False, dividerthres
# Create the the text to add, apply the indent, and add to the output
spacer = ' '*(maxkeylen-len(keystr))
if vallinecount == 1 or not showmultilines:
rawoutput = '#%i: "%s":%s %s\n' % (ind, keystr, spacer, valstr)
rawoutput = f'#{ind:d}: {quote}{keystr}{quote}{keysep}{spacer} {valstr}\n'
else:
rawoutput = '#%i: "%s":%s \n%s\n' % (ind, keystr, spacer, valstr)
rawoutput = f'#{ind:d}: {quote}{keystr}{quote}{keysep}{spacer} \n{valstr}\n'

# Perform the indentation.
newoutput = ut.indent(prefix=theprefix, text=rawoutput, width=80)
Expand All @@ -277,9 +277,9 @@ def __repr__(self, maxlen=None, showmultilines=True, divider=False, dividerthres
return output


def _repr_pretty_(self, p, cycle):
def _repr_pretty_(self, p, cycle, *args, **kwargs):
''' Function to fix __repr__ in IPython'''
print(self.__repr__())
print(self.__repr__(*args, **kwargs))


def disp(self, maxlen=None, showmultilines=True, divider=False, dividerthresh=10, numindents=0, sigfigs=5, numformat=None):
Expand Down Expand Up @@ -863,6 +863,11 @@ class objdict(odict):
>>> od.keys = 3 # This raises an exception (you can't overwrite the keys() method)
'''

def __repr__(self, *args, **kwargs):
''' Use odict repr, but with a custom class name and no quotes '''
return super().__repr__(quote='', keysep='.', classname='objdict()', *args, **kwargs)


def __getattribute__(self, attr):
try: # First, try to get the attribute as an attribute
output = odict.__getattribute__(self, attr)
Expand All @@ -886,7 +891,6 @@ def __setattr__(self, name, value):
errormsg = '"%s" exists as an attribute, so cannot be set as key; use setattribute() instead' % name
raise Exception(errormsg)

return None

def setattribute(self, name, value):
''' Set attribute if truly desired '''
Expand Down

0 comments on commit 9f1305e

Please sign in to comment.