cli2 3.3.46__tar.gz → 3.4.0__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 (50) hide show
  1. {cli2-3.3.46/cli2.egg-info → cli2-3.4.0}/PKG-INFO +1 -1
  2. {cli2-3.3.46 → cli2-3.4.0}/cli2/__init__.py +1 -1
  3. cli2-3.4.0/cli2/ansible.py +273 -0
  4. {cli2-3.3.46 → cli2-3.4.0}/cli2/client.py +74 -8
  5. {cli2-3.3.46 → cli2-3.4.0}/cli2/display.py +14 -2
  6. {cli2-3.3.46 → cli2-3.4.0}/cli2/example_client.py +11 -3
  7. cli2-3.4.0/cli2/locker.py +62 -0
  8. {cli2-3.3.46 → cli2-3.4.0}/cli2/logging.py +3 -3
  9. cli2-3.4.0/cli2/pytest_ansible.py +221 -0
  10. cli2-3.4.0/cli2/test_ansible.py +113 -0
  11. {cli2-3.3.46 → cli2-3.4.0}/cli2/test_client.py +132 -122
  12. cli2-3.4.0/cli2/test_restful.py +92 -0
  13. {cli2-3.3.46 → cli2-3.4.0/cli2.egg-info}/PKG-INFO +1 -1
  14. {cli2-3.3.46 → cli2-3.4.0}/cli2.egg-info/SOURCES.txt +5 -0
  15. {cli2-3.3.46 → cli2-3.4.0}/cli2.egg-info/entry_points.txt +3 -0
  16. {cli2-3.3.46 → cli2-3.4.0}/setup.py +4 -1
  17. {cli2-3.3.46 → cli2-3.4.0}/MANIFEST.in +0 -0
  18. {cli2-3.3.46 → cli2-3.4.0}/README.rst +0 -0
  19. {cli2-3.3.46 → cli2-3.4.0}/classifiers.txt +0 -0
  20. {cli2-3.3.46 → cli2-3.4.0}/cli2/argument.py +0 -0
  21. {cli2-3.3.46 → cli2-3.4.0}/cli2/asyncio.py +0 -0
  22. {cli2-3.3.46 → cli2-3.4.0}/cli2/cli.py +0 -0
  23. {cli2-3.3.46 → cli2-3.4.0}/cli2/colors.py +0 -0
  24. {cli2-3.3.46 → cli2-3.4.0}/cli2/command.py +0 -0
  25. {cli2-3.3.46 → cli2-3.4.0}/cli2/configuration.py +0 -0
  26. {cli2-3.3.46 → cli2-3.4.0}/cli2/decorators.py +0 -0
  27. {cli2-3.3.46 → cli2-3.4.0}/cli2/entry_point.py +0 -0
  28. {cli2-3.3.46 → cli2-3.4.0}/cli2/example_client_complex.py +0 -0
  29. {cli2-3.3.46 → cli2-3.4.0}/cli2/example_nesting.py +0 -0
  30. {cli2-3.3.46 → cli2-3.4.0}/cli2/example_obj.py +0 -0
  31. {cli2-3.3.46 → cli2-3.4.0}/cli2/group.py +0 -0
  32. {cli2-3.3.46 → cli2-3.4.0}/cli2/node.py +0 -0
  33. {cli2-3.3.46 → cli2-3.4.0}/cli2/overrides.py +0 -0
  34. {cli2-3.3.46 → cli2-3.4.0}/cli2/sphinx.py +0 -0
  35. {cli2-3.3.46 → cli2-3.4.0}/cli2/table.py +0 -0
  36. {cli2-3.3.46 → cli2-3.4.0}/cli2/test.py +0 -0
  37. {cli2-3.3.46 → cli2-3.4.0}/cli2/test_cli.py +0 -0
  38. {cli2-3.3.46 → cli2-3.4.0}/cli2/test_command.py +0 -0
  39. {cli2-3.3.46 → cli2-3.4.0}/cli2/test_configuration.py +0 -0
  40. {cli2-3.3.46 → cli2-3.4.0}/cli2/test_decorators.py +0 -0
  41. {cli2-3.3.46 → cli2-3.4.0}/cli2/test_display.py +0 -0
  42. {cli2-3.3.46 → cli2-3.4.0}/cli2/test_entry_point.py +0 -0
  43. {cli2-3.3.46 → cli2-3.4.0}/cli2/test_group.py +0 -0
  44. {cli2-3.3.46 → cli2-3.4.0}/cli2/test_inject.py +0 -0
  45. {cli2-3.3.46 → cli2-3.4.0}/cli2/test_node.py +0 -0
  46. {cli2-3.3.46 → cli2-3.4.0}/cli2/test_table.py +0 -0
  47. {cli2-3.3.46 → cli2-3.4.0}/cli2.egg-info/dependency_links.txt +0 -0
  48. {cli2-3.3.46 → cli2-3.4.0}/cli2.egg-info/requires.txt +0 -0
  49. {cli2-3.3.46 → cli2-3.4.0}/cli2.egg-info/top_level.txt +0 -0
  50. {cli2-3.3.46 → cli2-3.4.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: cli2
3
- Version: 3.3.46
3
+ Version: 3.4.0
4
4
  Summary: image:: https://yourlabs.io/oss/cli2/badges/master/pipeline.svg
5
5
  Home-page: https://yourlabs.io/oss/cli2
6
6
  Author: James Pic
@@ -28,7 +28,7 @@ except ImportError:
28
28
  # httpx not installed
29
29
  pass
30
30
  from .decorators import arg, cmd
31
- from .display import diff, print, highlight
31
+ from .display import diff, diff_data, render, print, highlight
32
32
  from .group import Group
33
33
  from .node import Node
34
34
  from .table import Table
@@ -0,0 +1,273 @@
1
+ """
2
+ Experimental: my base class for Ansible actions.
3
+
4
+ TODO: Turn this into a cool framework.
5
+ """
6
+
7
+ import asyncio
8
+ import cli2
9
+ import copy
10
+ import mock
11
+ import os
12
+ import re
13
+ import traceback
14
+
15
+ from cli2 import logging
16
+
17
+ from ansible.plugins.action import ActionBase
18
+
19
+ # colors:
20
+ # black
21
+ # bright gray
22
+ # blue
23
+ # white
24
+ # green
25
+ # cyan
26
+ # bright green
27
+ # red
28
+ # bright cyan
29
+ # purple
30
+ # bright red
31
+ # yellow
32
+ # bright purple
33
+ # dark gray
34
+ # magenta
35
+ # bright magenta
36
+ # normal
37
+
38
+ # 7-bit C1 ANSI sequences
39
+ ansi_escape = re.compile(r'''
40
+ \x1B # ESC
41
+ (?: # 7-bit C1 Fe (except CSI)
42
+ [@-Z\\-_]
43
+ | # or [ for CSI, followed by a control sequence
44
+ \[
45
+ [0-?]* # Parameter bytes
46
+ [ -/]* # Intermediate bytes
47
+ [@-~] # Final byte
48
+ )
49
+ ''', re.VERBOSE)
50
+
51
+
52
+ UNSET_DEFAULT = '__UNSET__DEFAULT__'
53
+
54
+
55
+ class Option:
56
+ """
57
+ Ansible Option descriptor.
58
+
59
+ .. py:attribute:: arg
60
+
61
+ Name of the task argument to get this option value from
62
+
63
+ .. py:attribute:: fact
64
+
65
+ Name of the fact, if any, to get a value for this option if no task arg
66
+ is provided
67
+
68
+ .. py:attribute:: default
69
+
70
+ Default value, if any, in case neither of arg and fact were defined.
71
+ """
72
+ UNSET_DEFAULT = UNSET_DEFAULT
73
+
74
+ def __init__(self, arg=None, fact=None, default=UNSET_DEFAULT):
75
+ self.arg = arg
76
+ self.fact = fact
77
+ self.default = default
78
+
79
+ @property
80
+ def kwargs(self):
81
+ kwargs = dict(default=self.default)
82
+ if self.arg:
83
+ kwargs['arg_name'] = self.arg
84
+ if self.fact:
85
+ kwargs['fact_name'] = self.fact
86
+ return kwargs
87
+
88
+ def __get__(self, obj, objtype=None):
89
+ if obj is None:
90
+ return self
91
+ try:
92
+ return obj.get(**self.kwargs)
93
+ except AttributeError:
94
+ raise AnsibleOptionError(self)
95
+
96
+
97
+ class AnsibleError(Exception):
98
+ pass
99
+
100
+
101
+ class AnsibleOptionError(AnsibleError):
102
+ def __init__(self, option):
103
+ self.option = option
104
+ super().__init__(option.kwargs)
105
+
106
+ @property
107
+ def message(self):
108
+ message = ['Missing']
109
+ if self.option.arg:
110
+ message.append(f'arg `{self.option.arg}`')
111
+ if self.option.fact:
112
+ message.append('or')
113
+ if self.option.fact:
114
+ message.append(f'fact `{self.option.fact}`')
115
+ return ' '.join(message)
116
+
117
+
118
+ class ActionBase(ActionBase):
119
+ """
120
+ Base action class
121
+
122
+ .. py:attribute:: result
123
+
124
+ Result dict that will be returned to Ansible
125
+
126
+ .. py:attribute:: task_vars
127
+
128
+ The task_vars that the module was called with
129
+
130
+ .. py:attribute:: client
131
+
132
+ The client object generated by :py:meth:`client_factory` if you
133
+ implement it.
134
+ """
135
+ def get(self, arg_name, fact_name=None, default=UNSET_DEFAULT):
136
+ if arg_name in self._task.args:
137
+ return self._task.args[arg_name]
138
+ if fact_name and fact_name in self.task_vars:
139
+ return self.task_vars[fact_name]
140
+ if default != UNSET_DEFAULT:
141
+ return default
142
+ if fact_name:
143
+ raise AttributeError(f'Undefined {arg_name} or {fact_name}')
144
+ else:
145
+ raise AttributeError(f'Undefined arg {arg_name}')
146
+
147
+ def run(self, tmp=None, task_vars=None):
148
+ self.tmp = tmp
149
+ self.task_vars = task_vars
150
+ self.result = super().run(tmp, task_vars)
151
+ asyncio.run(self.run_wrapped_async())
152
+ return self.result
153
+
154
+ async def run_wrapped_async(self):
155
+ self.verbosity = self.task_vars.get('ansible_verbosity', 0)
156
+
157
+ if 'LOG_LEVEL' in os.environ or 'DEBUG' in os.environ:
158
+ logging.configure()
159
+ else:
160
+ if self.verbosity == 1:
161
+ os.environ['LOG_LEVEL'] = 'INFO'
162
+ elif self.verbosity >= 2:
163
+ os.environ['LOG_LEVEL'] = 'DEBUG'
164
+ logging.configure()
165
+
166
+ try:
167
+ try:
168
+ self.client = await self.client_factory()
169
+ except NotImplementedError:
170
+ self.client = None
171
+ await self.run_async()
172
+ except Exception as exc:
173
+ self.result['failed'] = True
174
+
175
+ if isinstance(exc, AnsibleError):
176
+ self.result['error'] = exc.message
177
+ elif self.verbosity:
178
+ traceback.print_exc()
179
+
180
+ # for pytest to raise
181
+ self.exc = exc
182
+ finally:
183
+ if (
184
+ self._before_data != UNSET_DEFAULT
185
+ and self._after_data != UNSET_DEFAULT
186
+ ):
187
+ diff = cli2.diff_data(
188
+ self._before_data,
189
+ self._after_data,
190
+ self._before_label,
191
+ self._after_label,
192
+ )
193
+ if self.client and self.client.mask:
194
+ output = '\n'.join([
195
+ line.rstrip() for line in diff if line.strip()
196
+ ])
197
+ output = re.sub(
198
+ f'({"|".join(self.client.mask)}): (.*)',
199
+ '\\1: ***MASKED***',
200
+ ''.join(output),
201
+ )
202
+ print(cli2.highlight(output, 'Diff'))
203
+ else:
204
+ cli2.diff(diff)
205
+
206
+ async def run_async(self):
207
+ """
208
+ The method you are supposed to implement.
209
+
210
+ It should:
211
+
212
+ - provision the :py:attr:`result` dict
213
+ - find task_vars in :py:attr:`task_vars`
214
+ """
215
+
216
+ async def client_factory(self):
217
+ """
218
+ Return a client instance.
219
+
220
+ :raise NotImplementedError: By default
221
+ """
222
+ raise NotImplementedError()
223
+
224
+ @classmethod
225
+ async def run_test_async(cls, args=None, facts=None, client=None,
226
+ fail=False):
227
+ """
228
+ Test run the module in a mocked context.
229
+
230
+ :param args: Dict of task arguments
231
+ :param facts: Dict of play facts
232
+ :param client: Client instance, overrides the factory
233
+ :param fail: Allow this test to fail without exception
234
+ """
235
+ obj = cls(*[mock.Mock()] * 6)
236
+ obj.tmp = None
237
+ obj.task_vars = mock.Mock()
238
+ obj.result = dict()
239
+ obj._task = mock.Mock()
240
+ obj._task.args = args or {}
241
+ obj.task_vars = facts or {}
242
+ obj.task_vars['ansible_verbosity'] = 1
243
+ obj.exc = False
244
+ if client:
245
+ async def _factory():
246
+ return client
247
+ obj.client_factory = _factory
248
+ old = obj.client_factory
249
+
250
+ async def set_tries():
251
+ client = await old()
252
+ client.handler.tries = 0
253
+ return client
254
+ obj.client_factory = set_tries
255
+ await obj.run_wrapped_async()
256
+ if obj.exc and not fail:
257
+ raise obj.exc
258
+ if obj.result.get('failed', False) and not fail:
259
+ raise Exception('Module failed, and fail is not True {obj.result}')
260
+ return obj
261
+
262
+ def __init__(self, *args, **kwargs):
263
+ super().__init__(*args, **kwargs)
264
+ self._before_data = UNSET_DEFAULT
265
+ self._after_data = UNSET_DEFAULT
266
+
267
+ def before_set(self, data, label='before'):
268
+ self._before_data = copy.deepcopy(data)
269
+ self._before_label = label
270
+
271
+ def after_set(self, data, label='after'):
272
+ self._after_data = copy.deepcopy(data)
273
+ self._after_label = label
@@ -181,14 +181,12 @@ class Paginator:
181
181
  .. code-block:: python
182
182
 
183
183
  def pagination_parameters(self, params, page_number):
184
- # this is the default implementation
185
184
  params['page'] = page_number
186
185
 
187
186
  :param params: Dict of base GET parameters
188
187
  :param page_number: Page number to get
189
188
  """
190
- if page_number > 1:
191
- params['page'] = page_number
189
+ raise NotImplementedError("pagination_parameters not implemented")
192
190
 
193
191
  def response_items(self, response):
194
192
  """
@@ -231,7 +229,11 @@ class Paginator:
231
229
 
232
230
  :param page_number: Page number to get the items from
233
231
  """
234
- return self.response_items(await self.page_response(page_number))
232
+ try:
233
+ return self.response_items(await self.page_response(page_number))
234
+ except NotImplementedError:
235
+ # pagination_parameters not implemented, can't paginate
236
+ return []
235
237
 
236
238
  async def page_response(self, page_number):
237
239
  """
@@ -240,7 +242,11 @@ class Paginator:
240
242
  :param page_number: Page number to get the items from
241
243
  """
242
244
  params = self.params.copy()
243
- self.pagination_parameters(params, page_number)
245
+ try:
246
+ self.pagination_parameters(params, page_number)
247
+ except NotImplementedError:
248
+ if page_number > 1:
249
+ raise
244
250
  for expression in self.expressions:
245
251
  if expression.parameterable:
246
252
  expression.params(params)
@@ -365,6 +371,12 @@ class Field:
365
371
  - Use :py:meth:`internal_set` to actually set the internal
366
372
  :py:attr:`Model.data`
367
373
  """
374
+ try:
375
+ old_value = getattr(obj, self.name)
376
+ if self.name not in obj.changed_fields and value != old_value:
377
+ obj.changed_fields[self.name] = old_value
378
+ except FieldExternalizeError:
379
+ obj.changed_fields[self.name] = None
368
380
  value = self.internalize(obj, value)
369
381
  self.internal_set(obj, value)
370
382
 
@@ -678,6 +690,8 @@ class ModelGroup(Group):
678
690
 
679
691
  class ModelMetaclass(type):
680
692
  def __new__(cls, name, bases, attributes):
693
+ if 'Paginator' in attributes:
694
+ attributes['paginator'] = attributes['Paginator']
681
695
  cls = super().__new__(cls, name, bases, attributes)
682
696
  client = getattr(cls, 'client', None)
683
697
  if client:
@@ -769,9 +783,13 @@ class Model(metaclass=ModelMetaclass):
769
783
  self._dirty_fields = []
770
784
  self._field_cache = dict()
771
785
 
786
+ self.changed_fields = dict()
772
787
  for key, value in values.items():
773
788
  setattr(self, key, value)
774
789
 
790
+ # actually reset that
791
+ self.changed_fields = dict()
792
+
775
793
  @property
776
794
  def data(self):
777
795
  """
@@ -789,6 +807,10 @@ class Model(metaclass=ModelMetaclass):
789
807
  def data(self, value):
790
808
  self._data = value
791
809
 
810
+ @property
811
+ def data_masked(self):
812
+ return self.client.mask_data(self.data)
813
+
792
814
  @classmethod
793
815
  @hide('expressions')
794
816
  @cmd(color='green', condition=lambda cls: cls.url_list)
@@ -866,6 +888,7 @@ class Model(metaclass=ModelMetaclass):
866
888
  """
867
889
  response = await self.client.get(self.url)
868
890
  self.data.update(response.json())
891
+ self.changed_fields = dict()
869
892
 
870
893
  async def save(self):
871
894
  """
@@ -919,6 +942,8 @@ class Model(metaclass=ModelMetaclass):
919
942
 
920
943
  class ClientMetaclass(type):
921
944
  def __new__(cls, name, bases, attributes):
945
+ if 'Paginator' in attributes:
946
+ attributes['paginator'] = attributes['Paginator']
922
947
  cls = super().__new__(cls, name, bases, attributes)
923
948
 
924
949
  # bind ourself as _client_class to any inherited model
@@ -1046,13 +1071,52 @@ class ClientError(Exception):
1046
1071
 
1047
1072
 
1048
1073
  class ResponseError(ClientError):
1074
+ """
1075
+ Beautiful Response Error class.
1076
+
1077
+ .. py:attribute:: response
1078
+
1079
+ httpx Response object
1080
+
1081
+ .. py:attribute:: request
1082
+
1083
+ httpx Request object
1084
+
1085
+ .. py:attribute:: status_code
1086
+
1087
+ Response status code
1088
+
1089
+ .. py:attribute:: url
1090
+
1091
+ Request url
1092
+
1093
+ .. py:attribute:: method
1094
+
1095
+ Request method
1096
+ """
1049
1097
  def __init__(self, client, response, tries, mask, msg=None):
1050
1098
  self.client = client
1051
1099
  self.response = response
1052
1100
  self.tries = tries
1053
1101
  self.mask = mask
1054
- msg = msg or getattr(self, 'msg', '').format(self=self)
1055
- super().__init__(self.enhance(msg))
1102
+ self.msg = msg or getattr(self, 'msg', '').format(self=self)
1103
+ super().__init__(self.enhance(self.msg))
1104
+
1105
+ @property
1106
+ def request(self):
1107
+ return self.response.request
1108
+
1109
+ @property
1110
+ def method(self):
1111
+ return str(self.request.method)
1112
+
1113
+ @property
1114
+ def url(self):
1115
+ return str(self.request.url)
1116
+
1117
+ @property
1118
+ def status_code(self):
1119
+ return str(self.response.status_code)
1056
1120
 
1057
1121
  def enhance(self, msg):
1058
1122
  """
@@ -1444,7 +1508,9 @@ class Client(metaclass=ClientMetaclass):
1444
1508
 
1445
1509
  return response
1446
1510
 
1447
- def response_log_data(self, response, mask):
1511
+ def response_log_data(self, response, mask=None):
1512
+ if mask is None:
1513
+ mask = self.mask
1448
1514
  try:
1449
1515
  data = response.json()
1450
1516
  except json.JSONDecodeError:
@@ -4,8 +4,10 @@ Generic pretty display utils.
4
4
  This module defines a print function that's supposed to be able to pretty-print
5
5
  anything, as well as a pretty diff printer.
6
6
  """
7
+ import difflib
7
8
  import os
8
9
  import sys
10
+ import yaml
9
11
 
10
12
  try:
11
13
  import jsonlight as json
@@ -34,7 +36,6 @@ def highlight(string, lexer):
34
36
 
35
37
 
36
38
  def yaml_dump(data):
37
- import yaml
38
39
  if isinstance(data, dict):
39
40
  # ensure that objects inheriting from dict render nicely
40
41
  data = dict(data)
@@ -45,7 +46,7 @@ def yaml_highlight(yaml_string):
45
46
  return highlight(yaml_string, 'Yaml')
46
47
 
47
48
 
48
- def render(arg):
49
+ def render(arg, highlight=True):
49
50
  """
50
51
  Try to render arg as yaml.
51
52
 
@@ -78,6 +79,8 @@ def render(arg):
78
79
  pass
79
80
 
80
81
  string = arg if isinstance(arg, str) else yaml_dump(arg)
82
+ if not highlight:
83
+ return string
81
84
  return yaml_highlight(string)
82
85
 
83
86
 
@@ -111,3 +114,12 @@ def diff(diff, **kwargs):
111
114
  cli2.diff(difflib.unified_diff(old, new))
112
115
  """
113
116
  _print(diff_highlight(diff), **kwargs)
117
+
118
+
119
+ def diff_data(before, after, before_label='before', after_label='after'):
120
+ return difflib.unified_diff(
121
+ yaml.dump(before).splitlines(),
122
+ yaml.dump(after).splitlines(),
123
+ before_label,
124
+ after_label,
125
+ )
@@ -4,9 +4,17 @@ import cli2
4
4
  class APIClient(cli2.Client):
5
5
  """
6
6
  Client for restful-api.dev
7
+
8
+ Prior to using this, run at the root of this repository:
9
+
10
+ pip install django djangorestframework
11
+ ./manage.py migrate
12
+ ./manage.py runserver
7
13
  """
14
+ mask = ["Capacity"]
15
+
8
16
  def __init__(self, *args, **kwargs):
9
- kwargs.setdefault('base_url', 'https://api.restful-api.dev/')
17
+ kwargs.setdefault('base_url', 'http://localhost:8000')
10
18
  super().__init__(*args, **kwargs)
11
19
 
12
20
  @cli2.cmd
@@ -23,8 +31,8 @@ class Object(APIClient.Model):
23
31
 
24
32
  cli2-example-client object create name=cli2 capacity=2TB
25
33
  """
26
- url_list = '/objects'
27
- url_detail = '/objects/{self.id}'
34
+ url_list = '/objects/'
35
+ url_detail = '/objects/{self.id}/'
28
36
 
29
37
  id = cli2.Field()
30
38
  name = cli2.Field()
@@ -0,0 +1,62 @@
1
+ import fcntl
2
+ import os
3
+
4
+
5
+ class Locker:
6
+ def __init__(self, lock_path, blocking=True, prefix=None):
7
+ self.lock_path = lock_path
8
+ self.blocking = blocking
9
+ self.acquired = True
10
+ self.prefix = prefix or ''
11
+
12
+ def display(self, msg):
13
+ _ = []
14
+ if self.blocking:
15
+ _.append('[BLOCKING LOCK]')
16
+ else:
17
+ _.append('[NON BLOCKING LOCK]')
18
+ if self.prefix:
19
+ _.append(f'[{self.prefix}]')
20
+ _.append(f'[{self.lock_path}]')
21
+ _.append(f' {msg}')
22
+ self.print(''.join(_))
23
+
24
+ def print(self, msg):
25
+ print(msg)
26
+
27
+ def __enter__(self):
28
+ self.display('Waiting')
29
+ self.lock_path.parent.mkdir(parents=True, exists_ok=True)
30
+ self.fp = self.lock_path.open('w+')
31
+
32
+ if self.blocking:
33
+ fcntl.flock(self.fp.fileno(), fcntl.LOCK_EX)
34
+ self.display('Acquired')
35
+ return self
36
+
37
+ try:
38
+ fcntl.flock(
39
+ self.fp.fileno(),
40
+ fcntl.LOCK_EX | fcntl.LOCK_NB,
41
+ )
42
+ except BlockingIOError:
43
+ self.display('Not acquired but non blocking: proceeding')
44
+ self.acquired = False
45
+ else:
46
+ self.display('Acquired')
47
+ return self
48
+
49
+ def __exit__(self, _type, value, tb):
50
+ self.display('Releasing')
51
+ if self.acquired or not self.blocking:
52
+ fcntl.flock(self.fp.fileno(), fcntl.LOCK_UN)
53
+ self.fp.close()
54
+ self.display('Released')
55
+
56
+ if self.acquired:
57
+ try:
58
+ os.unlink(self.lock_path)
59
+ except FileNotFoundError:
60
+ pass # already deleted, or locked by another process
61
+ else:
62
+ self.display('Deleted')
@@ -15,9 +15,9 @@ class YAMLFormatter:
15
15
 
16
16
  def __call__(self, key, value):
17
17
  value = cli2.display.yaml_dump(value)
18
- if not self.colors:
19
- return value
20
- return cli2.display.yaml_highlight(value)
18
+ if self.colors:
19
+ value = cli2.display.yaml_highlight(value)
20
+ return '\n' + value
21
21
 
22
22
 
23
23
  def configure():