skilleter-modules 0.0.3__tar.gz → 0.0.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. {skilleter_modules-0.0.3/src/skilleter_modules.egg-info → skilleter_modules-0.0.6}/PKG-INFO +3 -1
  2. {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/pyproject.toml +6 -1
  3. {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/src/skilleter_modules/colour.py +0 -54
  4. {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/src/skilleter_modules/dc_curses.py +6 -4
  5. {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/src/skilleter_modules/dircolors.py +6 -1
  6. {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/src/skilleter_modules/docker.py +15 -8
  7. {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/src/skilleter_modules/files.py +4 -2
  8. {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/src/skilleter_modules/git.py +2 -2
  9. {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/src/skilleter_modules/path.py +4 -1
  10. {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/src/skilleter_modules/popup.py +5 -3
  11. {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/src/skilleter_modules/run.py +4 -3
  12. {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/src/skilleter_modules/tidy.py +2 -2
  13. {skilleter_modules-0.0.3 → skilleter_modules-0.0.6/src/skilleter_modules.egg-info}/PKG-INFO +3 -1
  14. {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/src/skilleter_modules.egg-info/SOURCES.txt +12 -2
  15. skilleter_modules-0.0.6/tests/test_colour.py +65 -0
  16. skilleter_modules-0.0.6/tests/test_dircolors.py +27 -0
  17. skilleter_modules-0.0.6/tests/test_docker.py +30 -0
  18. skilleter_modules-0.0.6/tests/test_files.py +22 -0
  19. skilleter_modules-0.0.6/tests/test_git.py +6 -0
  20. skilleter_modules-0.0.6/tests/test_gitlab.py +4 -0
  21. skilleter_modules-0.0.6/tests/test_path.py +23 -0
  22. skilleter_modules-0.0.6/tests/test_popup.py +70 -0
  23. skilleter_modules-0.0.6/tests/test_run.py +32 -0
  24. skilleter_modules-0.0.6/tests/test_tfm_pane.py +6 -0
  25. skilleter_modules-0.0.6/tests/test_tidy.py +10 -0
  26. skilleter_modules-0.0.3/src/skilleter_modules/tfm_pane.py +0 -593
  27. {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/LICENSE +0 -0
  28. {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/README.md +0 -0
  29. {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/setup.cfg +0 -0
  30. {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/src/skilleter_modules/__init__.py +0 -0
  31. {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/src/skilleter_modules/dc_defaults.py +0 -0
  32. {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/src/skilleter_modules/dc_util.py +0 -0
  33. {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/src/skilleter_modules/gitlab.py +0 -0
  34. {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/src/skilleter_modules/venv_template.py +0 -0
  35. {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/src/skilleter_modules.egg-info/dependency_links.txt +0 -0
  36. {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/src/skilleter_modules.egg-info/requires.txt +0 -0
  37. {skilleter_modules-0.0.3 → skilleter_modules-0.0.6}/src/skilleter_modules.egg-info/top_level.txt +0 -0
@@ -1,9 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: skilleter_modules
3
- Version: 0.0.3
3
+ Version: 0.0.6
4
4
  Summary: Modules used by my various Python projects (and hopefully useful to other people)
5
5
  Author-email: John Skilleter <john@skilleter.org.uk>
6
6
  Project-URL: Home, https://skilleter.org.uk
7
+ Project-URL: Repository, https://gitlab.com/skilleter/skilleter-modules
8
+ Project-URL: Issues, https://gitlab.com/skilleter/skilleter-extras/-/modules
7
9
  Classifier: Programming Language :: Python :: 3
8
10
  Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
9
11
  Classifier: Operating System :: OS Independent
@@ -7,7 +7,7 @@ name = "skilleter_modules"
7
7
 
8
8
  # Version must be incremented to install updated skilleter-extras
9
9
 
10
- version = "0.0.3"
10
+ version = "0.0.6"
11
11
 
12
12
  authors = [
13
13
  {name="John Skilleter", email="john@skilleter.org.uk"},
@@ -32,5 +32,10 @@ dependencies = [
32
32
 
33
33
  [project.urls]
34
34
  Home = "https://skilleter.org.uk"
35
+ Repository = "https://gitlab.com/skilleter/skilleter-modules"
36
+ Issues = "https://gitlab.com/skilleter/skilleter-extras/-/modules"
35
37
 
36
38
  [project.scripts]
39
+
40
+ [tool.pytest.ini_options]
41
+ pythonpath = ["src"]
@@ -272,57 +272,3 @@ def warning(txt, newline=True, stream=sys.stderr, prefix=False):
272
272
  write('[RED:WARNING]: ', newline=False, stream=stream, )
273
273
 
274
274
  write(txt, newline, stream)
275
-
276
- ################################################################################
277
-
278
- if __name__ == '__main__':
279
- for combo in (0, 1, 2):
280
- print()
281
- if combo == 0:
282
- print('Background colours')
283
- elif combo == 1:
284
- print('Foreground colours')
285
- else:
286
- print('Combinations')
287
-
288
- print()
289
- for y in range(0, 16):
290
- for x in range(0, 16):
291
- colour = x + y * 16
292
-
293
- if combo == 0:
294
- write(format('[B%d]%4d' % (colour, colour)), newline=False)
295
- elif combo == 1:
296
- write(format('[%d]%4d' % (colour, colour)), newline=False)
297
- else:
298
- write(format('[B%d]%4d[%d]/%4d ' % (colour, colour, 255 - colour, 255 - colour)), newline=False)
299
-
300
- write('[NORMAL]')
301
-
302
- print()
303
-
304
- write('Foreground: [RED]red [GREEN]green [BLACK]black [NORMAL]normal')
305
- write('Background: [BRED]red [BGREEN]green [BBLACK]black [NORMAL]normal')
306
-
307
- write('Foreground: [BBLUE:blue] [RED:red] normal')
308
-
309
- write('Bright foreground: [RED_B]red [GREEN_B]green [BLACK_B]black [NORMAL]normal')
310
- write('Bright background: [BRED_B]red [BGREEN_B]green [BBLACK_B]black [NORMAL]normal')
311
-
312
- write('Foreground: [BBLUE:blue_B] [RED:red_B] normal')
313
-
314
- print()
315
-
316
- write('[NORMAL:Normal text]')
317
- write('[FAINT:Faint text]')
318
- write('[ITALIC:Italic text]')
319
- write('[UNDERSCORE:Underscored text]')
320
- write('[BLINK:Blinking text]')
321
- write('[REVERSE:Reverse text]')
322
- write('[STRIKE:Strikethrough text]')
323
-
324
- print()
325
-
326
- error('Error message (nothing should be output after this)', status=0)
327
-
328
- write('This message should not appear')
@@ -79,19 +79,21 @@ class CursesDircolors:
79
79
  """ Given a set of attributes return the equivalent curses colour pair,
80
80
  creating a new one if a matching one doesn't already exsit """
81
81
 
82
- # TODO: Take account of attributes as well as colours
83
-
84
82
  colours = [attr['fore'], attr['back']]
85
83
 
86
84
  # Get an existing colour pair that uses the same colours or create
87
- # a new one if one doesn't exist
85
+ # a new one if one doesn't exist. If a pair slot is already allocated
86
+ # but the curses subsystem cannot allocate (e.g., no colours), skip init.
88
87
 
89
88
  if colours in self.colour_pairs:
90
89
  pair_index = self.colour_pairs.index(colours) + self.reserved
91
90
  else:
92
91
  pair_index = len(self.colour_pairs) + self.reserved
93
92
  self.colour_pairs.append(colours)
94
- curses.init_pair(pair_index, attr['fore'], attr['back'])
93
+ try:
94
+ curses.init_pair(pair_index, attr['fore'], attr['back'])
95
+ except curses.error:
96
+ pass
95
97
 
96
98
  return pair_index
97
99
 
@@ -131,8 +131,10 @@ class Dircolors:
131
131
  Returns a boolean indicating whether any data was loaded.
132
132
  The current database will always be cleared. """
133
133
  self.clear()
134
+ need_close = False
134
135
  if isinstance(database, str):
135
136
  file = open(database, 'r')
137
+ need_close = True
136
138
  elif isinstance(database, TextIOBase):
137
139
  file = database
138
140
  else:
@@ -157,6 +159,8 @@ class Dircolors:
157
159
  continue # ignore TERM directives
158
160
  elif key in _CODE_MAP:
159
161
  self._codes[_CODE_MAP[key]] = val
162
+ elif key.startswith('*.'):
163
+ self._extensions[key[1:]] = val
160
164
  elif key.startswith('.'):
161
165
  self._extensions[key] = val
162
166
  elif strict:
@@ -167,7 +171,8 @@ class Dircolors:
167
171
  self._loaded = True
168
172
  return self._loaded
169
173
  finally:
170
- file.close()
174
+ if need_close:
175
+ file.close()
171
176
 
172
177
  def load_defaults(self):
173
178
  """ Load the default database. """
@@ -35,8 +35,9 @@ def instances(all=False):
35
35
  try:
36
36
  process = subprocess.run(cmd, capture_output=True, check=True, text=True)
37
37
 
38
- for result in process.stdout:
39
- instances_list.append(result)
38
+ for result in process.stdout.splitlines():
39
+ if result:
40
+ instances_list.append(result)
40
41
 
41
42
  except subprocess.CalledProcessError as exc:
42
43
  raise DockerError(exc)
@@ -48,12 +49,17 @@ def instances(all=False):
48
49
  def stop(instance, force=False):
49
50
  """ Stop the specified Docker instance """
50
51
 
51
- # TODO: force option not implemented
52
+ cmd = ['docker', 'stop']
53
+
54
+ if force:
55
+ cmd.append('--force')
56
+
57
+ cmd.append(instance)
52
58
 
53
59
  try:
54
- subprocess.run(['docker', 'stop', instance], check=True, capture_output=False)
60
+ subprocess.run(cmd, check=True, capture_output=False)
55
61
 
56
- except suprocess.CalledProcessError as exc:
62
+ except subprocess.CalledProcessError as exc:
57
63
  raise DockerError(exc)
58
64
 
59
65
  ################################################################################
@@ -80,10 +86,11 @@ def images():
80
86
  """ Return a list of all current Docker images """
81
87
 
82
88
  try:
83
- process = subprocess.run(['docker', 'images', '-q'], capture_output=True, check=True)
89
+ process = subprocess.run(['docker', 'images', '-q'], capture_output=True, check=True, text=True)
84
90
 
85
- for result in process:
86
- yield result
91
+ for result in process.stdout.splitlines():
92
+ if result:
93
+ yield result
87
94
 
88
95
  except subprocess.CalledProcessError as exc:
89
96
  raise DockerError(exc)
@@ -19,7 +19,9 @@ def is_binary_file(filename):
19
19
  """ Return True if there is a strong likelihood that the specified file
20
20
  is binary. """
21
21
 
22
- return file_type(filename, mime=True).endswith('binary')
22
+ filetype = file_type(filename, mime=True)
23
+
24
+ return filetype.endswith('binary') if filetype else False
23
25
 
24
26
  ################################################################################
25
27
 
@@ -49,7 +51,7 @@ def format_size(size, always_suffix=False):
49
51
  """ Convert a memory/disk size into appropriately-scaled units in bytes,
50
52
  MiB, GiB, TiB as a string """
51
53
 
52
- # Keep all the maths positive
54
+ # Keep all the maths positive
53
55
 
54
56
  if size < 0:
55
57
  size = -size
@@ -933,7 +933,7 @@ def config_rm(section, key, source=LOCAL, path=None):
933
933
 
934
934
  ################################################################################
935
935
 
936
- def ref(fields=('objectname'), sort=None, remotes=False, path=None):
936
+ def ref(fields=('objectname',), sort=None, remotes=False, path=None):
937
937
  """ Wrapper for git for-each-ref """
938
938
 
939
939
  cmd = ['for-each-ref']
@@ -958,7 +958,7 @@ def ref(fields=('objectname'), sort=None, remotes=False, path=None):
958
958
  def branches(all=False, path=None, remote=False):
959
959
  """ Return a list of all the branches in the current repo """
960
960
 
961
- cmd = ['branch', '--format=%(refname:short)','--list']
961
+ cmd = ['branch', '--format=%(refname:short)', '--list']
962
962
 
963
963
  if all:
964
964
  cmd.append('--all')
@@ -32,7 +32,10 @@ def is_subdirectory(root_path, sub_path):
32
32
  logging.debug('root path: %s', abs_root_path)
33
33
  logging.debug('sub path : %s', abs_sub_path)
34
34
 
35
- return abs_sub_path.startswith('%s/' % abs_root_path)
35
+ common = os.path.commonpath([abs_root_path, abs_sub_path])
36
+
37
+ # Require a strict subdirectory: common path matches root and paths differ
38
+ return common == abs_root_path and abs_sub_path != abs_root_path
36
39
 
37
40
  ################################################################################
38
41
 
@@ -63,6 +63,11 @@ class PopUp():
63
63
 
64
64
  self.start_time = time.monotonic()
65
65
 
66
+ return self
67
+
68
+ def __exit__(self, _exc_type, _exc_value, _exc_traceback):
69
+ """ Remove the popup """
70
+
66
71
  if self.waitkey:
67
72
  while True:
68
73
  keypress = self.screen.getch()
@@ -72,9 +77,6 @@ class PopUp():
72
77
  curses.panel.update_panels()
73
78
  self.screen.refresh()
74
79
 
75
- def __exit__(self, _exc_type, _exc_value, _exc_traceback):
76
- """ Remove the popup """
77
-
78
80
  if self.panel:
79
81
  if self.sleep:
80
82
  elapsed = time.monotonic() - self.start_time
@@ -231,18 +231,19 @@ def _process(command,
231
231
 
232
232
  # Wait until the command terminates (and set the returncode)
233
233
 
234
+ cmd.wait()
235
+
234
236
  if stdout_thread:
235
237
  stdout_thread.join()
236
238
 
237
239
  if stderr_thread:
238
240
  stderr_thread.join()
239
241
 
240
- cmd.wait()
241
-
242
242
  # If the command failed, raise an exception
243
243
 
244
244
  if cmd.returncode:
245
- raise RunError('\n'.join(stderr_data) if stderr_data else 'Error %d running "%s"' % (cmd.returncode, ' '.join(command)))
245
+ raise RunError('\n'.join(stderr_data) if stderr_data else 'Error %d running "%s"' % (cmd.returncode, ' '.join(command)),
246
+ status=cmd.returncode)
246
247
 
247
248
  # Return status, stdout, stderr (the latter 2 may be empty if we did not capture data).
248
249
 
@@ -57,7 +57,7 @@ RE_TIME = [
57
57
  {'regex': re.compile(r'[0-9]{4}-[0-9]{2}-[0-9]{2}'), 'replace': '{DATE}'},
58
58
  {'regex': re.compile(r'[0-9]{2}-[0-9]{2}-[0-9]{4}'), 'replace': '{DATE}'},
59
59
 
60
- {'regex': re.compile(r'[0-9]([.][0-9]*)*\s*(second[s]?)'), 'replace': '{ELAPSED}'},
60
+ {'regex': re.compile(r'[0-9]+([.][0-9]*)*\s*(second[s]?)'), 'replace': '{ELAPSED}'},
61
61
 
62
62
  {'find': '{DATE} {TIME}', 'replace': '{DATE+TIME}'},
63
63
  {'regex': re.compile(r'[0-9]+m *[0-9]+s'), 'replace': '{ELAPSED}'},
@@ -83,7 +83,7 @@ RE_AWS = \
83
83
  {'regex': re.compile(r'vol-0[0-9a-f]{16}'), 'replace': '{AMI-VOL}'},
84
84
  {'regex': re.compile(r'sir-[0-9a-z]{8}'), 'replace': '{SPOT-INSTANCE}'},
85
85
  {'regex': re.compile(r'i-0[0-9a-f]{16}'), 'replace': '{EC2-ID}'},
86
- {'regex': re.compile(r'request id: [0-0a-f]{8}-[0-0a-f]{4}-[0-0a-f]{4}-[0-0a-f]{4}-[0-0a-f]{12}'),
86
+ {'regex': re.compile(r'request id: [0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'),
87
87
  'replace': 'request id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'},
88
88
  ]
89
89
 
@@ -1,9 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: skilleter_modules
3
- Version: 0.0.3
3
+ Version: 0.0.6
4
4
  Summary: Modules used by my various Python projects (and hopefully useful to other people)
5
5
  Author-email: John Skilleter <john@skilleter.org.uk>
6
6
  Project-URL: Home, https://skilleter.org.uk
7
+ Project-URL: Repository, https://gitlab.com/skilleter/skilleter-modules
8
+ Project-URL: Issues, https://gitlab.com/skilleter/skilleter-extras/-/modules
7
9
  Classifier: Programming Language :: Python :: 3
8
10
  Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
9
11
  Classifier: Operating System :: OS Independent
@@ -14,11 +14,21 @@ src/skilleter_modules/gitlab.py
14
14
  src/skilleter_modules/path.py
15
15
  src/skilleter_modules/popup.py
16
16
  src/skilleter_modules/run.py
17
- src/skilleter_modules/tfm_pane.py
18
17
  src/skilleter_modules/tidy.py
19
18
  src/skilleter_modules/venv_template.py
20
19
  src/skilleter_modules.egg-info/PKG-INFO
21
20
  src/skilleter_modules.egg-info/SOURCES.txt
22
21
  src/skilleter_modules.egg-info/dependency_links.txt
23
22
  src/skilleter_modules.egg-info/requires.txt
24
- src/skilleter_modules.egg-info/top_level.txt
23
+ src/skilleter_modules.egg-info/top_level.txt
24
+ tests/test_colour.py
25
+ tests/test_dircolors.py
26
+ tests/test_docker.py
27
+ tests/test_files.py
28
+ tests/test_git.py
29
+ tests/test_gitlab.py
30
+ tests/test_path.py
31
+ tests/test_popup.py
32
+ tests/test_run.py
33
+ tests/test_tfm_pane.py
34
+ tests/test_tidy.py
@@ -0,0 +1,65 @@
1
+ """Minimal test code for colour"""
2
+
3
+ import io
4
+
5
+ import skilleter_modules.colour as colour
6
+
7
+ def test_colour():
8
+ """Very basic test"""
9
+
10
+ for combo in (0, 1, 2):
11
+ print()
12
+ if combo == 0:
13
+ print('Background colours')
14
+ elif combo == 1:
15
+ print('Foreground colours')
16
+ else:
17
+ print('Combinations')
18
+
19
+ print()
20
+ for y in range(0, 16):
21
+ for x in range(0, 16):
22
+ colour_index = x + y * 16
23
+
24
+ if combo == 0:
25
+ colour.write(format('[B%d]%4d' % (colour_index, colour_index)), newline=False)
26
+ elif combo == 1:
27
+ colour.write(format('[%d]%4d' % (colour_index, colour_index)), newline=False)
28
+ else:
29
+ colour.write(format('[B%d]%4d[%d]/%4d ' % (colour_index, colour_index, 255 - colour_index, 255 - colour_index)), newline=False)
30
+
31
+ colour.write('[NORMAL]')
32
+
33
+ print()
34
+
35
+ colour.write('Foreground: [RED]red [GREEN]green [BLACK]black [NORMAL]normal')
36
+ colour.write('Background: [BRED]red [BGREEN]green [BBLACK]black [NORMAL]normal')
37
+
38
+ colour.write('Foreground: [BBLUE:blue] [RED:red] normal')
39
+
40
+ colour.write('Bright foreground: [RED_B]red [GREEN_B]green [BLACK_B]black [NORMAL]normal')
41
+ colour.write('Bright background: [BRED_B]red [BGREEN_B]green [BBLACK_B]black [NORMAL]normal')
42
+
43
+ colour.write('Foreground: [BBLUE:blue_B] [RED:red_B] normal')
44
+
45
+ print()
46
+
47
+ colour.write('[NORMAL:Normal text]')
48
+ colour.write('[FAINT:Faint text]')
49
+ colour.write('[ITALIC:Italic text]')
50
+ colour.write('[UNDERSCORE:Underscored text]')
51
+ colour.write('[BLINK:Blinking text]')
52
+ colour.write('[REVERSE:Reverse text]')
53
+ colour.write('[STRIKE:Strikethrough text]')
54
+
55
+
56
+ def test_write_empty_string_no_indent():
57
+ buf = io.StringIO()
58
+ colour.write('', indent=4, stream=buf)
59
+ assert buf.getvalue() == '\n'
60
+
61
+
62
+ def test_write_with_indent_and_strip():
63
+ buf = io.StringIO()
64
+ colour.write(' text', indent=2, strip=True, stream=buf)
65
+ assert buf.getvalue() == ' text\n'
@@ -0,0 +1,27 @@
1
+ import io
2
+ import stat
3
+
4
+ import skilleter_modules.dircolors as dircolors
5
+
6
+
7
+ def test_load_from_dircolors_does_not_close_stream():
8
+ data = io.StringIO('DIR 01;34\n')
9
+ dc = dircolors.Dircolors(load=False)
10
+ assert dc.load_from_dircolors(data, strict=True) is True
11
+ assert data.closed is False
12
+ assert dc.loaded is True
13
+
14
+
15
+ def test_format_mode_directory_and_extension(tmp_path):
16
+ dc = dircolors.Dircolors(load=False)
17
+ # Load minimal rules: directories blue (01;34), *.txt red (31)
18
+ rules = 'DIR 01;34\n*.txt 31\n'
19
+ assert dc.load_from_dircolors(io.StringIO(rules), strict=True)
20
+
21
+ # Directory formatting: should wrap with escape codes
22
+ formatted_dir = dc.format_mode('name', (stat.S_IFDIR | stat.S_IRUSR))
23
+ assert '\x1b[' in formatted_dir and formatted_dir.endswith('\x1b[0m')
24
+
25
+ # Extension formatting: .txt uses red foreground
26
+ formatted_file = dc.format_mode('file.txt', 0)
27
+ assert '\x1b[' in formatted_file and formatted_file.endswith('\x1b[0m')
@@ -0,0 +1,30 @@
1
+ import types
2
+
3
+ import pytest
4
+
5
+ import skilleter_modules.docker as docker
6
+
7
+
8
+ class DummyResult(types.SimpleNamespace):
9
+ pass
10
+
11
+
12
+ def test_instances_parses_lines(monkeypatch):
13
+ def fake_run(cmd, capture_output, check, text):
14
+ assert cmd[:2] == ['docker', 'ps']
15
+ return DummyResult(stdout='id1\nid2\n')
16
+
17
+ monkeypatch.setattr('subprocess.run', fake_run)
18
+ assert docker.instances() == ['id1', 'id2']
19
+
20
+
21
+ def test_stop_force_passes_flag(monkeypatch):
22
+ seen = {}
23
+
24
+ def fake_run(cmd, check, capture_output):
25
+ seen['cmd'] = cmd
26
+ return DummyResult()
27
+
28
+ monkeypatch.setattr('subprocess.run', fake_run)
29
+ docker.stop('abc', force=True)
30
+ assert seen['cmd'] == ['docker', 'stop', '--force', 'abc']
@@ -0,0 +1,22 @@
1
+ import os
2
+
3
+ import skilleter_modules.files as files
4
+
5
+
6
+ def test_format_size_suffix_and_sign():
7
+ assert files.format_size(-1024, always_suffix=True).startswith('-')
8
+ assert files.format_size(1024, always_suffix=True).endswith(' KiB')
9
+
10
+
11
+ def test_backup_no_file_no_raise(tmp_path):
12
+ missing = tmp_path / 'missing.txt'
13
+ # Should no-op when file is absent
14
+ files.backup(str(missing))
15
+ assert not list(tmp_path.iterdir())
16
+
17
+
18
+ def test_backup_creates_copy(tmp_path):
19
+ src = tmp_path / 'file.txt'
20
+ src.write_text('data', encoding='utf-8')
21
+ files.backup(str(src))
22
+ assert (tmp_path / 'file.bak').read_text(encoding='utf-8') == 'data'
@@ -0,0 +1,6 @@
1
+ import pytest
2
+
3
+ git = pytest.importorskip('skilleter_modules.git', reason='pygit2 not installed')
4
+
5
+ def test_git():
6
+ pass
@@ -0,0 +1,4 @@
1
+ import skilleter_modules.gitlab as gitlab
2
+
3
+ def test_gitlab():
4
+ pass
@@ -0,0 +1,23 @@
1
+ import skilleter_modules.path as path
2
+
3
+
4
+ def test_is_subdirectory_strict_descendant():
5
+ assert path.is_subdirectory('/root', '/root/child') is True
6
+
7
+
8
+ def test_is_subdirectory_same_path_is_false():
9
+ assert path.is_subdirectory('/root', '/root') is False
10
+
11
+
12
+ def test_is_subdirectory_unrelated_is_false():
13
+ assert path.is_subdirectory('/root/one', '/root/two') is False
14
+
15
+
16
+ def test_trimpath_home_to_tilde(monkeypatch):
17
+ monkeypatch.setenv('HOME', '/home/user')
18
+ assert path.trimpath('/home/user/projects', 80).startswith('~/')
19
+
20
+
21
+ def test_trimpath_truncates_middle():
22
+ trimmed = path.trimpath('/a/b/c/d/e/f/g/h/i', 10)
23
+ assert '...' in trimmed
@@ -0,0 +1,70 @@
1
+ from types import SimpleNamespace
2
+
3
+ import skilleter_modules.popup as popup
4
+
5
+
6
+ class FakePanel:
7
+ def __init__(self):
8
+ self.update_calls = 0
9
+ self.new_calls = []
10
+
11
+ def new_panel(self, window):
12
+ self.new_calls.append(window)
13
+ return SimpleNamespace(top=lambda: None)
14
+
15
+ def update_panels(self):
16
+ self.update_calls += 1
17
+
18
+
19
+ class FakeWindow:
20
+ def __init__(self):
21
+ self.add_calls = []
22
+ self.bkgd_calls = []
23
+
24
+ def bkgd(self, ch, colour):
25
+ self.bkgd_calls.append((ch, colour))
26
+
27
+ def addstr(self, y_pos, x_pos, line, colour):
28
+ self.add_calls.append((y_pos, x_pos, line, colour))
29
+
30
+
31
+ class FakeScreen:
32
+ def __init__(self, keys):
33
+ self.keys = list(keys)
34
+ self.getch_calls = 0
35
+ self.refresh_calls = 0
36
+
37
+ def getmaxyx(self):
38
+ return (24, 80)
39
+
40
+ def refresh(self):
41
+ self.refresh_calls += 1
42
+
43
+ def getch(self):
44
+ self.getch_calls += 1
45
+ return self.keys.pop(0)
46
+
47
+
48
+ def test_waitkey_occurs_on_exit(monkeypatch):
49
+ fake_panel = FakePanel()
50
+
51
+ fake_curses = SimpleNamespace(
52
+ KEY_RESIZE=999,
53
+ panel=fake_panel,
54
+ )
55
+
56
+ fake_curses.color_pair = lambda colour: f'colour-{colour}'
57
+ fake_curses.newwin = lambda h, w, y, x: FakeWindow()
58
+
59
+ monkeypatch.setattr(popup, 'curses', fake_curses)
60
+
61
+ keys = [fake_curses.KEY_RESIZE, ord('a')]
62
+ screen = FakeScreen(keys)
63
+
64
+ with popup.PopUp(screen, 'hello', 3, waitkey=True):
65
+ enter_updates = fake_panel.update_calls
66
+ assert screen.getch_calls == 0
67
+
68
+ assert fake_panel.update_calls == enter_updates + 1
69
+ assert screen.getch_calls == 2
70
+ assert screen.refresh_calls >= 1
@@ -0,0 +1,32 @@
1
+ import subprocess
2
+
3
+ import skilleter_modules.run as run
4
+
5
+
6
+ def test_process_echoes_and_captures_stdout(monkeypatch):
7
+ real_popen = subprocess.Popen
8
+
9
+ def fake_popen(command, bufsize=0, stdout=None, stderr=None, text=False, errors=None, encoding=None, **kwargs):
10
+ assert stdout == subprocess.PIPE
11
+ assert stderr == subprocess.PIPE
12
+ proc = real_popen(['printf', 'hello\n'], stdout=stdout, stderr=stderr, text=True)
13
+ return proc
14
+
15
+ monkeypatch.setattr(subprocess, 'Popen', fake_popen)
16
+ result = run._process(['printf', 'hello'])
17
+ assert result['stdout'] == ['hello']
18
+
19
+
20
+ def test_process_raises_on_nonzero(monkeypatch):
21
+ real_popen = subprocess.Popen
22
+
23
+ def fake_popen(command, bufsize=0, stdout=None, stderr=None, text=False, errors=None, encoding=None, **kwargs):
24
+ return real_popen(['python', '-c', 'import sys; sys.exit(3)'], stdout=stdout, stderr=stderr, text=True)
25
+
26
+ monkeypatch.setattr(subprocess, 'Popen', fake_popen)
27
+ try:
28
+ run._process(['python', '-c', 'exit 3'])
29
+ except run.RunError as exc:
30
+ assert exc.status == 3
31
+ else:
32
+ assert False, 'expected RunError'
@@ -0,0 +1,6 @@
1
+ import pytest
2
+
3
+ tfm_pane = pytest.importorskip('skilleter_modules.tfm_pane', reason='inotify not installed')
4
+
5
+ def test_tfm_pane():
6
+ pass
@@ -0,0 +1,10 @@
1
+ import skilleter_modules.tidy as tidy
2
+
3
+
4
+ def test_remove_times_does_not_match_stray_dot():
5
+ text = 'Version 1. release'
6
+ assert tidy.remove_times(text) == text
7
+
8
+
9
+ def test_remove_times_replaces_seconds():
10
+ assert tidy.remove_times('took 12.5 seconds') == 'took {ELAPSED}'
@@ -1,593 +0,0 @@
1
- ################################################################################
2
- """ Pane class for tfm """
3
- ################################################################################
4
-
5
- import sys
6
- import os
7
- import curses
8
- import fnmatch
9
- import stat
10
- import glob
11
- import time
12
- import threading
13
-
14
- from enum import IntEnum
15
-
16
- if sys.platform == 'linux':
17
- import inotify.adapters
18
-
19
- from . import dc_curses
20
- from . import path
21
- from . import popup
22
-
23
- ################################################################################
24
-
25
- class SortOrder(IntEnum):
26
- """ Sort order for filename list """
27
-
28
- FILENAME = 0
29
- EXTENSION = 1
30
- MODIFIED_DATE = 2
31
- SIZE = 3
32
- NUM_SORTS = 4
33
-
34
- SORT_TYPE = ('filename', 'extension', 'modified date', 'size')
35
-
36
- ################################################################################
37
-
38
- def inotify_wait(self):
39
- """Thread to wait for inotify events and post an event to the queue if there
40
- any create/delete/modify events in the current directory.
41
- Sends no more than 1 update per second to avoid drowning the recipient."""
42
-
43
- while True:
44
- trigger = False
45
- for event in self.ino.event_gen(yield_nones=False, timeout_s=1):
46
- (_, events, path, _) = event
47
-
48
- if path == self.current_dir and ('IN_CREATE' in events or 'IN_DELETE' in events or 'IN_MODIFY' in events):
49
- trigger = True
50
-
51
- if trigger:
52
- self.event_queue.put(('inotify', self.index))
53
-
54
- ################################################################################
55
-
56
- class Pane():
57
- """ Class for a file manager pane """
58
-
59
- def __init__(self, index, num_panes, colours, event_queue):
60
- # Create window for the pane (dummy size and position initially)
61
-
62
- self.screen = curses.newwin(1, 1, 0, 0)
63
-
64
- self.index = index
65
-
66
- self.current_dir = ''
67
-
68
- self.ino = inotify.adapters.Inotify() if sys.platform == 'linux' else None
69
-
70
- self.set_current_dir(os.getcwd())
71
-
72
- self.event_queue = event_queue
73
-
74
- if sys.platform == 'linux':
75
- inotify_thread = threading.Thread(target=inotify_wait, args=(self,), daemon=True)
76
- inotify_thread.start()
77
-
78
- # Default sort order
79
-
80
- self.sort_order = SortOrder.FILENAME
81
- self.reverse_sort = False
82
-
83
- # Set the attributes of the current review (some are initialised
84
- # when the screen is drawn)
85
-
86
- # Index of the current file in the filtered_file_indices
87
-
88
- self.current = 0
89
-
90
- self.offset = 0
91
- self.num_panes = num_panes
92
- self.colours = colours
93
-
94
- self.searchstring = None
95
-
96
- self.height = self.width = -1
97
- self.file_list_y = 1
98
- self.file_list_h = -1
99
-
100
- # File list is a list of the files in the current directory
101
-
102
- self.file_list = []
103
-
104
- # Filtered file list is a list of the indices in file_list of the visible files
105
- # in the current directory
106
-
107
- self.filtered_file_indices = []
108
-
109
- # Set of the names of currently-tagged files
110
-
111
- self.tagged_set = set()
112
-
113
- self.in_filter = self.out_filter = None
114
- self.hide_hidden_filter = True
115
-
116
- self.file_display_fields = ['size', 'mtime']
117
-
118
- # Set up dircolor highlighting
119
-
120
- self.dircolours = dc_curses.CursesDircolors(reserved=self.colours['reserved_colours'])
121
-
122
- # Generate the list of files to be shown (takes filtering into account)
123
-
124
- self.update_files()
125
-
126
- ################################################################################
127
-
128
- def sort_file_list(self):
129
- """ Sort the file list according to the current sort order """
130
-
131
- if self.sort_order == SortOrder.FILENAME:
132
- self.file_list.sort(reverse=self.reverse_sort, key=lambda entry: (not entry['isdir'], os.path.basename(entry['name'])))
133
- elif self.sort_order == SortOrder.EXTENSION:
134
- self.file_list.sort(reverse=self.reverse_sort, key=lambda entry: (not entry['isdir'], entry['name'].split('.')[-1]))
135
- elif self.sort_order == SortOrder.MODIFIED_DATE:
136
- self.file_list.sort(reverse=self.reverse_sort, key=lambda entry: (not entry['isdir'], entry['mtime']))
137
- elif self.sort_order == SortOrder.SIZE:
138
- self.file_list.sort(reverse=self.reverse_sort, key=lambda entry: (not entry['isdir'], entry['size']))
139
-
140
- ################################################################################
141
-
142
- def update_files(self):
143
- """ Get the list of files
144
- """
145
-
146
- def file_stats(filename):
147
- """ Get the stats for a file """
148
-
149
- filestat = os.stat(filename, follow_symlinks=False)
150
-
151
- info = {'name': filename,
152
- 'mode': filestat.st_mode,
153
- 'uid': filestat.st_uid,
154
- 'gid': filestat.st_gid,
155
- 'size': filestat.st_size,
156
- 'atime': filestat.st_atime,
157
- 'mtime': filestat.st_mtime,
158
- 'ctime': filestat.st_ctime,
159
- 'isdir': stat.S_ISDIR(filestat.st_mode)}
160
-
161
- return info
162
-
163
- # Rebuild the file list
164
-
165
- self.file_list = []
166
- for filename in glob.glob(os.path.join(self.current_dir, '*')) + glob.glob(os.path.join(self.current_dir, '.*')):
167
- self.file_list.append(file_stats(filename))
168
-
169
- # Update the tagged file list to contain only current files
170
-
171
- self.tagged_set = {entry['name'] for entry in self.file_list if entry['name'] in self.tagged_set}
172
-
173
- # Optionally add '..' as an entry
174
-
175
- if self.current_dir != '/':
176
- self.file_list.append(file_stats('..'))
177
-
178
- self.sort_file_list()
179
- self.update_file_list()
180
-
181
- ################################################################################
182
-
183
- def update_file_list(self):
184
- """ Generate the file list from the list of current files with filtering
185
- applied if enabled """
186
-
187
- self.sort_file_list()
188
-
189
- if self.active_filters():
190
- self.filtered_file_indices = [i for i, entry in enumerate(self.file_list) if not self.filtered(entry)]
191
- else:
192
- self.filtered_file_indices = range(len(self.file_list))
193
-
194
- ################################################################################
195
-
196
- def active_filters(self):
197
- """ Return true if any filters are active """
198
-
199
- return self.out_filter or \
200
- self.in_filter or \
201
- self.hide_hidden_filter
202
-
203
- ################################################################################
204
-
205
- def filtered(self, entry):
206
- """ Return True if an entry is hidden by one or more filters """
207
-
208
- result = False
209
-
210
- if self.out_filter and fnmatch.fnmatch(entry['name'], self.out_filter):
211
- result = True
212
-
213
- elif self.in_filter and not fnmatch.fnmatch(entry['name'], self.in_filter):
214
- result = True
215
-
216
- elif self.hide_hidden_filter:
217
- base_name = os.path.basename(entry['name'])
218
- if base_name[0] == '.' and base_name != '..':
219
- result = True
220
-
221
- return result
222
-
223
- ################################################################################
224
-
225
- def constrain_display_parameters(self):
226
- """ Ensure that the current display parameters are within range - easier
227
- to do it in one place for all of them than check individually whenever we
228
- change any of them """
229
-
230
- self.current = max(min(self.current, len(self.filtered_file_indices) - 1), 0)
231
- self.offset = min(len(self.filtered_file_indices) - 1, max(0, self.offset))
232
-
233
- # Keep the current entry on-screen
234
-
235
- if self.current >= self.offset + self.height - 2:
236
- self.offset = self.current
237
- elif self.current < self.offset:
238
- self.offset = self.current
239
-
240
- ################################################################################
241
-
242
- def file_info_display(self, filename):
243
- """ Extract the additional file info fields displayed to the right
244
- of the filename """
245
-
246
- data = []
247
- for field in self.file_display_fields:
248
- if field == 'name':
249
- data.append(filename['name'])
250
- elif field in ('atime', 'mtime', 'ctime'):
251
- data.append(time.strftime('%x %X', time.gmtime(filename[field])))
252
- elif field == 'uid':
253
- pass
254
- elif field == 'gid':
255
- pass
256
- elif field == 'mode':
257
- pass
258
- elif field == 'size':
259
- data.append(str(filename[field]))
260
-
261
- return ' '.join(data)
262
-
263
- ################################################################################
264
-
265
- def show_file_list(self, current_pane):
266
- """ Draw the current page of the file list """
267
-
268
- for ypos in range(0, self.file_list_h):
269
-
270
- normal_colour = curses.color_pair(self.colours['normal'])
271
-
272
- if 0 <= self.offset + ypos < len(self.filtered_file_indices):
273
- # Work out what colour to render the file details in
274
-
275
- current_file = self.file_list[self.filtered_file_indices[self.offset + ypos]]
276
-
277
- current = self.offset + ypos == self.current
278
-
279
- # The text to render
280
-
281
- filename = os.path.basename(current_file['name'])
282
-
283
- data = self.file_info_display(current_file)
284
-
285
- name = f'/{filename}' if current_file['isdir'] else filename
286
- name = f'* {name}' if current_file['name'] in self.tagged_set else f' {name}'
287
-
288
- if len(name) > self.width - len(data):
289
- entry = name[:self.width - 3] + '...'
290
- else:
291
- entry = name + ' ' * (self.width - len(name) - len(data)) + data
292
-
293
- file_colour = self.dircolours.get_colour_pair(current_file['name'], current_file['mode'])
294
- else:
295
- filename = entry = None
296
- current = False
297
- file_colour = normal_colour
298
-
299
- # Reverse the colours if this the cursor line
300
-
301
- if current and current_pane:
302
- file_colour |= curses.A_REVERSE
303
- normal_colour |= curses.A_REVERSE
304
-
305
- # Write the prefix, filename, and, if necessary, padding
306
-
307
- self.screen.move(self.file_list_y + ypos, 0)
308
- if entry:
309
- self.screen.addstr(entry, file_colour)
310
- else:
311
- self.screen.clrtoeol()
312
-
313
- # if len(filename) < self.width:
314
- # self.screen.addstr(self.file_list_y + ypos, len(filename), ' ' * (self.width - len(filename)), normal_colour)
315
-
316
- current_dir = path.trimpath(self.current_dir, self.width)
317
-
318
- self.screen.move(0, 0)
319
- self.screen.attron(curses.color_pair(self.colours['status']))
320
- self.screen.addstr(current_dir + ' '*(self.width-len(current_dir)))
321
-
322
- self.screen.refresh()
323
-
324
- if not self.filtered_file_indices:
325
- with popup.PopUp(self.screen, 'All files are hidden - Press \'c\' to clear filters.', self.colours['status']):
326
- pass
327
-
328
- ################################################################################
329
-
330
- def filter_description(self):
331
- """ Return a textual description of the active filters """
332
-
333
- filters = []
334
-
335
- if self.out_filter:
336
- filters.append('filter-out wildcard')
337
-
338
- if self.in_filter:
339
- filters.append('filter-in wildcard')
340
-
341
- return ', '.join(filters)
342
-
343
- ################################################################################
344
-
345
- def clear_filters(self):
346
- """ Clear all filters """
347
-
348
- if self.out_filter or self.in_filter:
349
- self.out_filter = self.in_filter = None
350
- self.update_file_list()
351
-
352
- ################################################################################
353
-
354
- def reload_changes(self):
355
- """ Update the list of files in case something external has
356
- changed it. """
357
-
358
- self.update_files()
359
-
360
- ################################################################################
361
-
362
- def get_current_dir(self):
363
- """ Get the current directory for the pane """
364
-
365
- return self.current_dir
366
-
367
- ################################################################################
368
-
369
- def set_current_dir(self, directory):
370
- """ Set the current directory for the pane """
371
-
372
- if self.current_dir and self.ino:
373
- self.ino.remove_watch(self.current_dir)
374
-
375
- self.current_dir = os.path.normpath(directory)
376
-
377
- if self.ino:
378
- self.ino.add_watch(directory)
379
-
380
- ################################################################################
381
-
382
- def get_current_file(self):
383
- """ Get the current file for the pane """
384
-
385
- return self.file_list[self.filtered_file_indices[self.current]]
386
-
387
- ################################################################################
388
-
389
- def get_tagged_files(self):
390
- """ Get the list of tagged files, or the current file if none are tagged """
391
-
392
- if self.tagged_set:
393
- return [self.file_list[entry] for entry in self.filtered_file_indices if self.file_list[entry]['name'] in self.tagged_set]
394
-
395
- return [self.get_current_file()]
396
-
397
- ################################################################################
398
-
399
- def search_entry(self, searchstring):
400
- """ Search for the next match with the specified search string """
401
-
402
- for i in list(range(self.current + 1, len(self.filtered_file_indices))) + list(range(0, self.current)):
403
- if fnmatch.fnmatch(os.path.basename(self.file_list[self.filtered_file_indices[i]]['name']), searchstring):
404
- self.current = i
405
- break
406
-
407
- ################################################################################
408
-
409
- def search_match(self, searchstring):
410
- """ Search for the first match """
411
-
412
- self.searchstring = searchstring
413
- self.search_next_match()
414
-
415
- ################################################################################
416
-
417
- def search_next_match(self):
418
- """ Search for the next match with the current search string """
419
-
420
- self.search_entry(self.searchstring)
421
-
422
- ################################################################################
423
-
424
- def move_end(self):
425
- """ Move to the end of the file list """
426
-
427
- self.current = len(self.filtered_file_indices) - 1
428
-
429
- ################################################################################
430
-
431
- def move_top(self):
432
- """ Move to the top of the file list """
433
-
434
- self.current = self.offset = 0
435
-
436
- ################################################################################
437
-
438
- def move_to_file(self, filename):
439
- """ Move to the specified file (if it exists) in the current directory
440
- or to the top if not """
441
-
442
- self.current = self.offset = 0
443
- if filename:
444
- self.search_entry(filename)
445
-
446
- ################################################################################
447
-
448
- def move(self, delta):
449
- """ Move up or down the file list """
450
-
451
- self.current += delta
452
-
453
- ################################################################################
454
-
455
- def filter_out(self, filter_out):
456
- """ Set an exclusion filter """
457
-
458
- self.out_filter = filter_out
459
- self.in_filter = None
460
- self.update_file_list()
461
-
462
- ################################################################################
463
-
464
- def filter_in(self, filter_in):
465
- """ Set an inclusion filter """
466
-
467
- self.in_filter = filter_in
468
- self.out_filter = None
469
- self.update_file_list()
470
-
471
- ################################################################################
472
-
473
- def move_page_down(self):
474
- """ Page down """
475
-
476
- pos = self.current - self.offset
477
- self.offset += self.file_list_h - 1
478
- self.current = self.offset + pos
479
-
480
- ################################################################################
481
-
482
- def move_page_up(self):
483
- """ Page up """
484
-
485
- pos = self.current - self.offset
486
- self.offset -= self.file_list_h - 1
487
- self.current = self.offset + pos
488
-
489
- ################################################################################
490
-
491
- def set_sort_order(self, value):
492
- """ Set the sort order """
493
-
494
- self.sort_order = (self.sort_order + value) % SortOrder.NUM_SORTS
495
-
496
- self.update_sort()
497
-
498
- ################################################################################
499
-
500
- def get_sort_order(self):
501
- """ Get the current sort order """
502
-
503
- return self.sort_order
504
-
505
- ################################################################################
506
-
507
- def sort_type_msg(self):
508
- """ Return a textual explanation of the current sort type """
509
-
510
- if self.reverse_sort:
511
- msg = f'Reverse-sorting by {SORT_TYPE[self.sort_order]}'
512
- else:
513
- msg = f'Sorting by {SORT_TYPE[self.sort_order]}'
514
-
515
- return msg
516
-
517
- ################################################################################
518
-
519
- def reverse_sort_order(self):
520
- """ Reverse the sort order """
521
-
522
- self.reverse_sort = not self.reverse_sort
523
- self.update_sort()
524
-
525
- ################################################################################
526
-
527
- def update_sort(self):
528
- """ Update the sort """
529
-
530
- msg = self.sort_type_msg()
531
-
532
- with popup.PopUp(self.screen, msg, self.colours['status']):
533
- self.update_file_list()
534
-
535
- ################################################################################
536
-
537
- def set_pane_coords(self, y, x, height, width):
538
- """ Set the pane height given the pane display area """
539
-
540
- pane_width = width//self.num_panes
541
-
542
- self.height = height
543
- self.file_list_h = height-1
544
- self.width = pane_width-1 # TODO: Why '-1'?
545
- self.screen.resize(height, pane_width)
546
- self.screen.mvwin(y, x + pane_width*self.index)
547
-
548
- ################################################################################
549
-
550
- def tag_current(self):
551
- """ Tag the current entry (unless it is '..') """
552
-
553
- current = self.file_list[self.filtered_file_indices[self.current]]['name']
554
-
555
- if current != '..':
556
- if current in self.tagged_set:
557
- self.tagged_set.remove(current)
558
- else:
559
- self.tagged_set.add(current)
560
-
561
- ################################################################################
562
-
563
- def untag(self, wildcard=None):
564
- """ Tag all, or selected tagged items """
565
-
566
- if wildcard:
567
- remove_tags = set()
568
- for entry in self.tagged_set:
569
- if fnmatch.fnmatch(self.filtered_file_indices[entry], wildcard):
570
- remove_tags.add(entry)
571
-
572
- self.tagged_set -= remove_tags
573
- else:
574
- self.tagged_set = set()
575
-
576
- ################################################################################
577
-
578
- def get_hidden_visibility(self):
579
- """ Return the current state of hidden file visibility """
580
-
581
- return not self.hide_hidden_filter
582
-
583
- ################################################################################
584
-
585
- def set_hidden_visibility(self, state=False):
586
- """ Set the visibility of hidden files """
587
-
588
- self.hide_hidden_filter = not state
589
-
590
- change_txt = 'Hiding' if self.hide_hidden_filter else 'Showing'
591
-
592
- with popup.PopUp(self.screen, f'{change_txt} hidden files', self.colours['status']):
593
- self.update_file_list()