Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Slicing IO read_ routines #586

Merged
merged 7 commits into from
Jun 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ we hit release version 1.0.0.
## [X.Y.Z] - YYYY-MM-DD

### Added
- slicing io files multiple output (still WIP), see #584 for details
Intention is to have all methods use this method for returning
multiple values, it should streamline the API.
- allowed xyz files to read Origin entries in the comment field
- allowed sile specifiers to be more explicit:
- "hello.xyz{contains=<name>}" equivalent to "hello.xyz{<name>}"
Expand Down Expand Up @@ -40,6 +43,7 @@ we hit release version 1.0.0.
- `BrillouinZone.merge` allows simple merging of several objects, #537

### Changed
- `stdoutSileOrca` will not accept `all=` arguments, see #584
- `xyzSile` out from sisl will now default to the extended xyz file-format
Explicitly adding the nsc= value makes it compatible with other exyz
file formats and parseable by sisl, this is an internal change
Expand Down
263 changes: 263 additions & 0 deletions src/sisl/io/_multiple.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
from functools import (
update_wrapper,
reduce
)
from textwrap import dedent
from typing import Any, Callable, Optional, Type
from numbers import Integral

Func = Callable[..., Optional[Any]]


class SileSlicer:
""" Handling io-methods in sliced behaviour for multiple returns

This class handler can expose a slicing behavior of the function
that it applies to.

The idea is to attach a function/method to this class and
let this perform the function at hand for slicing behaviour
etc.
"""
def __init__(self,
obj: Type[Any],
func: Func,
key: Type[Any],
*,
skip_func: Optional[Func]=None,
postprocess: Optional[Callable[..., Any]]=None):
# this makes it work like a function bound to an instance (func.__self__
# works for instances)
self.__self__ = obj
self.__func__ = func
self.key = key
if skip_func is None:
self.skip_func = func
else:
self.skip_func = skip_func
if postprocess is None:
def postprocess(ret):
return ret
self.postprocess = postprocess
zerothi marked this conversation as resolved.
Show resolved Hide resolved
# this is already sliced, sub-slicing shouldn't work (at least for now)
update_wrapper(self, func)

def __call__(self, *args, **kwargs):
""" Defer call to the function """
# Now handle the arguments
obj = self.__self__
func = self.__func__
key = self.key
skip_func = self.skip_func

# quick return if no slicing
if key is None:
return func(obj, *args, **kwargs)

inf = 100000000000000

def check_none(r):
if isinstance(r, tuple):
return reduce(lambda x, y: x and y is None, r, True)
return r is None

# Determine whether we can reduce the call overheads
start = 0
stop = inf

if isinstance(key, Integral):
if key >= 0:
start = key
stop = key + 1
elif key.step is None or key.step > 0: # step size of 1
if key.start is not None:
start = key.start
if key.stop is not None:
stop = key.stop
elif key.step < 0:
if key.stop is not None:
start = key.stop
if key.start is not None:
stop = key.start

if start < 0:
start = 0
if stop < 0:
stop = inf
assert stop >= start

# collect returning values
retvals = [None] * start
append = retvals.append
with obj: # open sile
# quick-skip using the skip-function
for _ in range(start):
skip_func(obj, *args, **kwargs)

# now do actual parsing
retval = func(obj, *args, **kwargs)
while not check_none(retval):
append(retval)
if len(retvals) >= stop:
# quick exit
break
retval = func(obj, *args, **kwargs)

if len(retvals) == start:
# none has been found
return None

# ensure the next call won't use this key
# This will prohibit the use
# tmp = sile.read_geometry[:10]
# tmp() # will return the first 10
# tmp() # will return the default (single) item
self.key = None
if isinstance(key, Integral):
return retvals[key]

# else postprocess
return self.postprocess(retvals[key])

def __get__(self, obj, objtype=None):
# I have no idea why this is needed, if I don't have this, then
# the help(read_geometry) returns without the correct interface,
# while if it is present it gets the correct interface.
# But it will not be called? I.e. a print statement will never occur
pass


class SileBound:
""" A bound method deferring stuff to the function

This class calls the function `func` when directly called
but returns the `slicer` class when users slices this object.
"""
def __init__(self,
obj: Type[Any],
func: Callable[..., Any],
*,
slicer: Type[SileSlicer]=SileSlicer,
default_slice: Optional[Any]=None,
**kwargs):
self.__self__ = obj
self.__func__ = func
self.slicer = slicer
self.default_slice = default_slice
self.kwargs = kwargs
update_wrapper(self, func)
self._update_doc()

def _update_doc(self):
# Override name to display slice handling in help
default_slice = self.default_slice
if self.default_slice is None:
default_slice = 0

self.__name__ = f"{self.__name__}[...|{default_slice!r}]"
name = self.__func__.__name__
try:
doc = self.__doc__
except AttributeError:
doc = ""

if default_slice == 0:
default_slice = "the first"
elif default_slice == -1:
default_slice = "the last"
elif default_slice == slice(None):
default_slice = "all"
else:
default_slice = self.default_slice

doc = dedent(f"""{doc}
Notes
-----
This method defaults to return {default_slice} item(s).

This method enables slicing for handling multiple values (see [...|default]).

This is an optional handler enabling returning multiple elements if {name}
allows this.

>>> single = obj.{name}() # returns the default entry of {name}

To retrieve the first two elements that {name} will return

>>> first_two = obj.{name}[:2]()

Retrieving the last two is done equivalently:

>>> last_two = obj.{name}[-2:]()

While one can store the sliced function ``tmp = obj.{name}[:]`` one
will loose the slice after each call.
""")
try:
self.__doc__ = doc
except AttributeError:
Fixed Show fixed Hide fixed
# we cannot set the __doc__ string, let it go
pass

def __call__(self, *args, **kwargs):
if self.default_slice is None:
return self.__func__(self.__self__, *args, **kwargs)
return self[self.default_slice](*args, **kwargs)

def __getitem__(self, key):
return self.slicer(
obj=self.__self__,
func=self.__func__,
key=key,
**self.kwargs
)

@property
def next(self):
return self[0]

@property
def last(self):
return self[-1]

def __get__(self, obj, objtype=None):
# I have no idea why this is needed, if I don't have this, then
# the help(read_geometry) returns without the correct interface,
# while if it is present it gets the correct interface.
# But it will not be called? I.e. a print statement will never occur
pass


class SileBinder:
""" Bind a class instance to the function name it decorates

Enables to bypass a class method with another object to defer
handling in specific cases.
"""
def __init__(self, **kwargs):
self.kwargs = kwargs

# this is the decorator call
def __call__(self, func):
self.__func__ = func
# update doc str etc.
update_wrapper(self, func)
return self

def __get__(self, obj, objtype=None):
func = self.__func__
if obj is None:
raise TypeError(f"[{self.__class__.__name__}]{objtype.__name__}.{func.__name__} missing (at least) 1 required positional argument: 'self'")
bound = SileBound(
obj=obj,
func=func,
**self.kwargs
)

# bind the class object to the host
setattr(obj, func.__name__, bound)
return bound

4 changes: 1 addition & 3 deletions src/sisl/io/openmx/md.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@

@set_module("sisl.io.openmx")
class mdSileOpenMX(xyzSile, SileOpenMX):

def read_geometry(self, *args, all=True, **kwargs):
return super().read_geometry(*args, all=all, **kwargs)
pass


add_sile('md', mdSileOpenMX, gzip=True)
Loading