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

Update objects when path changes for MultiFileSelector and FileSelector #546

Closed
wants to merge 9 commits into from
6 changes: 4 additions & 2 deletions examples/user_guide/Parameter_Types.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -745,9 +745,11 @@
"\n",
"A `param.ListSelector` works just the same as a regular Selector, but the value is a _list_ of valid objects from the available objects, rather than just one. Each item in the list is checked against the `objects`, and thus the current value is thus a _subset_ of the `objects`, rather than just one of the objects.\n",
"\n",
"A `param.FileSelector` works like a regular Selector with the value being a filename and the `objects` being computed from files on a file system. The files are specified as a `path` [glob](https://docs.python.org/3/library/glob.html), and all filenames matching the glob are valid `objects` for the parameter.\n",
"A `param.FileSelector` works like a Selector with the value being a filename and the `objects` being computed from files on a file system. The files are specified as a `path` [glob](https://docs.python.org/3/library/glob.html), and all filenames matching the glob are valid `objects` for the parameter. The default value is *None* unless specified.\n",
"\n",
"A `param.MultiFileSelector` is the analog of ListSelector but for files, i.e., again supporting a path glob but allowing the user to select a list of filenames rather than a single filename. The default value in this case is _all_ of the matched files, not just the first one."
"A `param.MultiFileSelector` is the analog of ListSelector but for files, i.e., again supporting a path glob but allowing the user to select a list of filenames rather than a single filename. The default value in this case is _all_ of the matched files, not just the first one. The default value is `None` unless specified.\n",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This appears to say two different values for the default, which presumably can't be true?

"\n",
"Both `param.FileSelector` and `param.MultiFileSelector` have an `update` method that sets `objects` with the results of a *glob* on the current `path`. This can be useful when the filesystem has changed. When `update` is called the default value of `param.FileSelector` is set to the first path of `objects` and the default value of `param.MultiFileSelector` is set equal to `objects`."
]
},
{
Expand Down
12 changes: 1 addition & 11 deletions param/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1275,7 +1275,7 @@ def _validate(self, val):
limiter = ', ...]'
break
items = '[' + ', '.join(items) + limiter
raise ValueError("%s not in parameter%s's list of possible objects, "
raise ValueError("%r not in parameter%s's list of possible objects, "
"valid options include %s" % (val, attrib_name, items))

def _ensure_value_is_in_objects(self,val):
Expand Down Expand Up @@ -1827,11 +1827,6 @@ def __init__(self, default=None, path="", **kwargs):
super(FileSelector, self).__init__(default=default, objects=self.objects,
empty_default=True, **kwargs)

def _on_set(self, attribute, old, new):
super(FileSelector, self)._on_set(attribute, new, old)
if attribute == 'path':
self.update()

def update(self):
self.objects = sorted(glob.glob(self.path))
if self.default in self.objects:
Expand Down Expand Up @@ -1879,11 +1874,6 @@ def __init__(self, default=None, path="", **kwargs):
self.update()
super(MultiFileSelector, self).__init__(default=default, objects=self.objects, **kwargs)

def _on_set(self, attribute, old, new):
super(MultiFileSelector, self)._on_set(attribute, new, old)
if attribute == 'path':
self.update()

def update(self):
self.objects = sorted(glob.glob(self.path))
if self.default and all([o in self.objects for o in self.default]):
Expand Down
106 changes: 106 additions & 0 deletions tests/API1/testfileselector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import os
import shutil
import tempfile

import param

from . import API1TestCase


class TestFileSelectorParameters(API1TestCase):

def setUp(self):
super(TestFileSelectorParameters, self).setUp()

tmpdir1 = tempfile.mkdtemp()
fa = os.path.join(tmpdir1, 'a.txt')
fb = os.path.join(tmpdir1, 'b.txt')
glob1 = os.path.join(tmpdir1, '*')
open(fa, 'w').close()
open(fb, 'w').close()
tmpdir2 = tempfile.mkdtemp()
fc = os.path.join(tmpdir2, 'c.txt')
fd = os.path.join(tmpdir2, 'd.txt')
glob2 = os.path.join(tmpdir2, '*')
open(fc, 'w').close()
open(fd, 'w').close()

self.tmpdir1 = tmpdir1
self.tmpdir2 = tmpdir2
self.fa = fa
self.fb = fb
self.fc = fc
self.fd = fd
self.glob1 = glob1
self.glob2 = glob2

class P(param.Parameterized):
a = param.FileSelector(path=glob1)
b = param.FileSelector(default=fa, path=glob1)

self.P = P

def tearDown(self):
shutil.rmtree(self.tmpdir1)
shutil.rmtree(self.tmpdir2)

def test_default_is_None(self):
p = self.P()
assert p.a is None
assert p.param.a.default is None

def test_default_is_honored(self):
p = self.P()
assert p.b == self.fa
assert p.param.b.default in [self.fa, self.fb]

def test_allow_default_None(self):
class P(param.Parameterized):
a = param.FileSelector(default=None)

def test_default_not_in_glob(self):
with self.assertRaises(ValueError):
class P(param.Parameterized):
a = param.FileSelector(default='not/in/glob', path=self.glob1)

def test_objects_auto_set(self):
p = self.P()
assert p.param.a.objects == [self.fa, self.fb]

def test_set_object_constructor(self):
p = self.P(a=self.fb)
assert p.a == self.fb

def test_set_object_outside_bounds(self):
p = self.P()
with self.assertRaises(ValueError):
p.a = '/not/in/glob'

def test_set_path_and_update(self):
p = self.P()
p.param.b.path = self.glob2
p.param.b.update()
assert p.param.b.objects == [self.fc, self.fd]
assert p.param.b.default in [self.fc, self.fd]
# Default updated but not the value itself
assert p.b == self.fa

def test_get_range(self):
p = self.P()
r = p.param.a.get_range()
assert r['a.txt'] == self.fa
assert r['b.txt'] == self.fb
p.param.a.path = self.glob2
p.param.a.update()
r = p.param.a.get_range()
assert r['c.txt'] == self.fc
assert r['d.txt'] == self.fd

def test_update_file_removed(self):
p = self.P()
assert p.param.b.objects == [self.fa, self.fb]
assert p.param.b.default in [self.fa, self.fb]
os.remove(self.fa)
p.param.b.update()
assert p.param.b.objects == [self.fb]
assert p.param.b.default == self.fb
110 changes: 110 additions & 0 deletions tests/API1/testmultifileselector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import os
import shutil
import tempfile

import param

from . import API1TestCase


class TestMultiFileSelectorParameters(API1TestCase):

def setUp(self):
super(TestMultiFileSelectorParameters, self).setUp()

tmpdir1 = tempfile.mkdtemp()
fa = os.path.join(tmpdir1, 'a.txt')
fb = os.path.join(tmpdir1, 'b.txt')
glob1 = os.path.join(tmpdir1, '*')
open(fa, 'w').close()
open(fb, 'w').close()
tmpdir2 = tempfile.mkdtemp()
fc = os.path.join(tmpdir2, 'c.txt')
fd = os.path.join(tmpdir2, 'd.txt')
glob2 = os.path.join(tmpdir2, '*')
open(fc, 'w').close()
open(fd, 'w').close()

self.tmpdir1 = tmpdir1
self.tmpdir2 = tmpdir2
self.fa = fa
self.fb = fb
self.fc = fc
self.fd = fd
self.glob1 = glob1
self.glob2 = glob2

class P(param.Parameterized):
a = param.MultiFileSelector(path=glob1)
b = param.MultiFileSelector(default=[fa], path=glob1)

self.P = P

def tearDown(self):
shutil.rmtree(self.tmpdir1)
shutil.rmtree(self.tmpdir2)

def test_default_is_None(self):
p = self.P()
assert p.a is None
assert p.param.a.default is None

def test_default_is_honored(self):
p = self.P()
assert p.b == [self.fa]
assert p.param.b.default ==[self.fa]

def test_allow_default_None(self):
class P(param.Parameterized):
a = param.FileSelector(default=None)

def test_objects_auto_set(self):
p = self.P()
assert p.param.a.objects == [self.fa, self.fb]

def test_default_not_in_glob(self):
with self.assertRaises(ValueError):
class P(param.Parameterized):
a = param.FileSelector(default=['not/in/glob'], path=self.glob1)

def test_objects_auto_set(self):
p = self.P()
assert sorted(p.param.a.objects) == sorted([self.fa, self.fb])

def test_set_object_constructor(self):
p = self.P(a=[self.fb])
assert p.a == [self.fb]

def test_set_object_outside_bounds(self):
p = self.P()
with self.assertRaises(ValueError):
p.a = ['/not/in/glob']

def test_set_path_and_update(self):
p = self.P()
p.param.b.path = self.glob2
p.param.b.update()
assert sorted(p.param.b.objects) == sorted([self.fc, self.fd])
assert sorted(p.param.b.default) == sorted([self.fc, self.fd])
# Default updated but not the value itself
assert p.b == [self.fa]

def test_get_range(self):
p = self.P()
r = p.param.a.get_range()
assert r['a.txt'] == self.fa
assert r['b.txt'] == self.fb
p.param.a.path = self.glob2
p.param.a.update()
r = p.param.a.get_range()
assert r['c.txt'] == self.fc
assert r['d.txt'] == self.fd

def test_update_file_removed(self):
p = self.P()
assert p.param.b.objects == [self.fa, self.fb]
assert p.param.b.default == [self.fa]
os.remove(self.fa)
p.param.b.update()
assert p.param.b.objects == [self.fb]
assert p.param.b.default == [self.fb]