cli2 4.1.3__tar.gz → 4.2.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 (56) hide show
  1. {cli2-4.1.3/cli2.egg-info → cli2-4.2.0}/PKG-INFO +1 -1
  2. {cli2-4.1.3 → cli2-4.2.0}/cli2/__init__.py +2 -0
  3. {cli2-4.1.3 → cli2-4.2.0}/cli2/cli.py +22 -7
  4. {cli2-4.1.3 → cli2-4.2.0}/cli2/client.py +95 -45
  5. {cli2-4.1.3 → cli2-4.2.0/cli2.egg-info}/PKG-INFO +1 -1
  6. {cli2-4.1.3 → cli2-4.2.0}/setup.py +1 -1
  7. {cli2-4.1.3 → cli2-4.2.0}/tests/test_client.py +34 -10
  8. {cli2-4.1.3 → cli2-4.2.0}/MANIFEST.in +0 -0
  9. {cli2-4.1.3 → cli2-4.2.0}/README.rst +0 -0
  10. {cli2-4.1.3 → cli2-4.2.0}/classifiers.txt +0 -0
  11. {cli2-4.1.3 → cli2-4.2.0}/cli2/ansible/__init__.py +0 -0
  12. {cli2-4.1.3 → cli2-4.2.0}/cli2/ansible/action.py +0 -0
  13. {cli2-4.1.3 → cli2-4.2.0}/cli2/ansible/playbook.py +0 -0
  14. {cli2-4.1.3 → cli2-4.2.0}/cli2/ansible/variables.py +0 -0
  15. {cli2-4.1.3 → cli2-4.2.0}/cli2/asyncio.py +0 -0
  16. {cli2-4.1.3 → cli2-4.2.0}/cli2/cli2.py +0 -0
  17. {cli2-4.1.3 → cli2-4.2.0}/cli2/colors.py +0 -0
  18. {cli2-4.1.3 → cli2-4.2.0}/cli2/configuration.py +0 -0
  19. {cli2-4.1.3 → cli2-4.2.0}/cli2/decorators.py +0 -0
  20. {cli2-4.1.3 → cli2-4.2.0}/cli2/display.py +0 -0
  21. {cli2-4.1.3 → cli2-4.2.0}/cli2/examples/__init__.py +0 -0
  22. {cli2-4.1.3 → cli2-4.2.0}/cli2/examples/client.py +0 -0
  23. {cli2-4.1.3 → cli2-4.2.0}/cli2/examples/conf.py +0 -0
  24. {cli2-4.1.3 → cli2-4.2.0}/cli2/examples/example.py +0 -0
  25. {cli2-4.1.3 → cli2-4.2.0}/cli2/examples/example_obj.py +0 -0
  26. {cli2-4.1.3 → cli2-4.2.0}/cli2/examples/nesting.py +0 -0
  27. {cli2-4.1.3 → cli2-4.2.0}/cli2/examples/obj.py +0 -0
  28. {cli2-4.1.3 → cli2-4.2.0}/cli2/examples/obj2.py +0 -0
  29. {cli2-4.1.3 → cli2-4.2.0}/cli2/examples/test.py +0 -0
  30. {cli2-4.1.3 → cli2-4.2.0}/cli2/lock.py +0 -0
  31. {cli2-4.1.3 → cli2-4.2.0}/cli2/log.py +0 -0
  32. {cli2-4.1.3 → cli2-4.2.0}/cli2/node.py +0 -0
  33. {cli2-4.1.3 → cli2-4.2.0}/cli2/sphinx.py +0 -0
  34. {cli2-4.1.3 → cli2-4.2.0}/cli2/table.py +0 -0
  35. {cli2-4.1.3 → cli2-4.2.0}/cli2/test.py +0 -0
  36. {cli2-4.1.3 → cli2-4.2.0}/cli2.egg-info/SOURCES.txt +0 -0
  37. {cli2-4.1.3 → cli2-4.2.0}/cli2.egg-info/dependency_links.txt +0 -0
  38. {cli2-4.1.3 → cli2-4.2.0}/cli2.egg-info/entry_points.txt +0 -0
  39. {cli2-4.1.3 → cli2-4.2.0}/cli2.egg-info/requires.txt +0 -0
  40. {cli2-4.1.3 → cli2-4.2.0}/cli2.egg-info/top_level.txt +0 -0
  41. {cli2-4.1.3 → cli2-4.2.0}/setup.cfg +0 -0
  42. {cli2-4.1.3 → cli2-4.2.0}/tests/test_ansible.py +0 -0
  43. {cli2-4.1.3 → cli2-4.2.0}/tests/test_ansible_variables.py +0 -0
  44. {cli2-4.1.3 → cli2-4.2.0}/tests/test_asyncio.py +0 -0
  45. {cli2-4.1.3 → cli2-4.2.0}/tests/test_cli.py +0 -0
  46. {cli2-4.1.3 → cli2-4.2.0}/tests/test_command.py +0 -0
  47. {cli2-4.1.3 → cli2-4.2.0}/tests/test_configuration.py +0 -0
  48. {cli2-4.1.3 → cli2-4.2.0}/tests/test_decorators.py +0 -0
  49. {cli2-4.1.3 → cli2-4.2.0}/tests/test_display.py +0 -0
  50. {cli2-4.1.3 → cli2-4.2.0}/tests/test_entry_point.py +0 -0
  51. {cli2-4.1.3 → cli2-4.2.0}/tests/test_group.py +0 -0
  52. {cli2-4.1.3 → cli2-4.2.0}/tests/test_inject.py +0 -0
  53. {cli2-4.1.3 → cli2-4.2.0}/tests/test_lock.py +0 -0
  54. {cli2-4.1.3 → cli2-4.2.0}/tests/test_node.py +0 -0
  55. {cli2-4.1.3 → cli2-4.2.0}/tests/test_restful.py +0 -0
  56. {cli2-4.1.3 → cli2-4.2.0}/tests/test_table.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: cli2
3
- Version: 4.1.3
3
+ Version: 4.2.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
@@ -24,11 +24,13 @@ try:
24
24
  FieldValueError,
25
25
  FieldExternalizeError,
26
26
  Client,
27
+ ClientCommand,
27
28
  DateTimeField,
28
29
  Field,
29
30
  Handler,
30
31
  JSONStringField,
31
32
  Model,
33
+ ModelCommand,
32
34
  Paginator,
33
35
  Related,
34
36
  )
@@ -658,6 +658,13 @@ class Command(EntryPoint, dict):
658
658
  self.exit_code = 1
659
659
  return self.help(missing=missing)
660
660
 
661
+ await self.factories_resolve()
662
+
663
+ result = self.call(*self.bound.args, **self.bound.kwargs)
664
+ return await async_resolve(result, output=True)
665
+
666
+ async def factories_resolve(self):
667
+ """ Resolve all factories values. """
661
668
  factories = self.values(factories=True)
662
669
  if factories:
663
670
  results = await asyncio.gather(*[
@@ -667,9 +674,6 @@ class Command(EntryPoint, dict):
667
674
  for _, arg in enumerate(factories):
668
675
  arg.value = results[_]
669
676
 
670
- result = self.call(*self.bound.args, **self.bound.kwargs)
671
- return await async_resolve(result, output=True)
672
-
673
677
  def ordered(self, factories=False):
674
678
  """
675
679
  Order the parameters by priority.
@@ -1153,7 +1157,7 @@ class Argument:
1153
1157
  self.value = self.cast(value)
1154
1158
  return True
1155
1159
 
1156
- def factory_value(self):
1160
+ def factory_value(self, cmd=None):
1157
1161
  """
1158
1162
  Run the factory function and return the value.
1159
1163
 
@@ -1163,15 +1167,26 @@ class Argument:
1163
1167
  If the factory function takes an `arg` argument, it will pass self.
1164
1168
 
1165
1169
  It will forward any argument to the factory function if detected in
1166
- it's signature.
1170
+ it's signature, except for ``*args`` and ``**kwargs``.
1171
+
1172
+ :param cmd: Override for :py:attr:`cmd`, useful for getting the factory
1173
+ value of an argument from another class (advanced).
1167
1174
  """
1168
1175
  kwargs = dict()
1176
+ cmd = cmd or self.cmd
1169
1177
  sig = inspect.signature(self.factory)
1170
1178
  if 'cmd' in sig.parameters:
1171
- kwargs['cmd'] = self.cmd
1179
+ kwargs['cmd'] = cmd
1172
1180
  if 'arg' in sig.parameters:
1173
1181
  kwargs['arg'] = self
1174
- for key, arg in self.cmd.items():
1182
+
1183
+ excluded = (
1184
+ inspect.Parameter.VAR_KEYWORD,
1185
+ inspect.Parameter.VAR_POSITIONAL,
1186
+ )
1187
+ for key, arg in cmd.items():
1188
+ if arg.param.kind in excluded:
1189
+ continue
1175
1190
  if key in sig.parameters:
1176
1191
  kwargs[key] = arg.value
1177
1192
  return self.factory(**kwargs)
@@ -23,7 +23,7 @@ except ImportError:
23
23
 
24
24
  from . import display
25
25
  from .asyncio import async_resolve
26
- from .cli import Command, Group, cmd, hide
26
+ from .cli import Argument, Command, Group, cmd, hide
27
27
  from .colors import colors
28
28
  from .log import log
29
29
 
@@ -663,34 +663,40 @@ class ModelCommand(Command):
663
663
  self.overrides['cls']['factory'] = self.get_model
664
664
 
665
665
  def setargs(self):
666
+ """
667
+ ModelCommand setargs which calls setargs on the client class and
668
+ defines an id argument for object commands.
669
+ """
666
670
  super().setargs()
671
+ self.group.parent.client_class.setargs(self)
667
672
  if 'self' in self:
668
673
  self.arg('id', position=0, kind='POSITIONAL_ONLY', doc='ID')
669
674
 
670
- async def get_client(self):
671
- client = self.group.parent.overrides['self']['factory']()
672
- return await async_resolve(client)
675
+ async def factories_resolve(self):
676
+ """
677
+ Return a client object from it's factory, will all args resolved.
678
+ """
679
+ # create a hidden Argument to use it's factory caller
680
+ argument = Argument(
681
+ self,
682
+ inspect.Parameter('_', kind=inspect.Parameter.POSITIONAL_ONLY),
683
+ factory=self.client_class.factory,
684
+ )
685
+ # this ensures the factory gets any kind of args
686
+ factory = await argument.factory_value(self)
687
+ self.client = await async_resolve(factory)
688
+ await super().factories_resolve()
673
689
 
674
690
  async def get_model(self):
675
- return getattr(await self.get_client(), self.model.__name__)
691
+ """ Return a client instance bound model """
692
+ return getattr(self.client, self.model.__name__)
676
693
 
677
694
  async def get_object(self):
678
695
  model = await self.get_model()
679
696
  return await model.get(id=self['id'].value)
680
697
 
681
-
682
- class ModelGroup(Group):
683
- def __init__(self, cls):
684
- super().__init__(
685
- name=cls.__name__.lower(),
686
- doc=inspect.getdoc(cls),
687
- cmdclass=type(
688
- 'ModelCommand',
689
- (cls.model_command,),
690
- dict(model=cls),
691
- )
692
- )
693
- self.load(cls)
698
+ async def post_call(self):
699
+ await self.client.post_call(self)
694
700
 
695
701
 
696
702
  class ModelMetaclass(type):
@@ -699,10 +705,14 @@ class ModelMetaclass(type):
699
705
  attributes['paginator'] = attributes['Paginator']
700
706
 
701
707
  cls = super().__new__(cls, name, bases, attributes)
702
- cls.Command = type(
708
+ client_class = getattr(cls, '_client_class', None)
709
+ cls.cmdclass = type(
703
710
  'ModelCommand',
704
- (cls.Command,),
705
- dict(model=cls),
711
+ (cls.cmdclass,),
712
+ dict(
713
+ model=cls,
714
+ client_class=client_class,
715
+ ),
706
716
  )
707
717
  client = getattr(cls, 'client', None)
708
718
  if client:
@@ -710,7 +720,6 @@ class ModelMetaclass(type):
710
720
  cls.paginator = client.paginator
711
721
  return cls
712
722
 
713
- client_class = getattr(cls, '_client_class', None)
714
723
  if client_class:
715
724
  client_class.models.append(cls)
716
725
 
@@ -744,7 +753,7 @@ class ModelMetaclass(type):
744
753
  cli_kwargs = dict(
745
754
  name=cls.__name__.lower(),
746
755
  doc=inspect.getdoc(cls),
747
- cmdclass=cls.Command,
756
+ cmdclass=cls.cmdclass,
748
757
  )
749
758
  cli_kwargs.update(cls.cli_kwargs)
750
759
  cls._cli = Group(**cli_kwargs)
@@ -789,24 +798,20 @@ class Model(metaclass=ModelMetaclass):
789
798
  Dict of kwargs to use to create the :py:class:`~cli2.cli.Group` for
790
799
  this model.
791
800
 
792
- .. py:attribute:: Command
801
+ .. py:attribute:: cmdclass
793
802
 
794
- Define a Command class inside the ModelClass to use a specific
795
- :py:class:`~cli2.cli.Command` class for a model.
796
- Don't inherit from anything: a new class will be generated for you
797
- inheriting from both :py:class:`~cls.cli2.client.ModelCommand` and
798
- :py:attr:`~cli2.cli.Client.Command`.
803
+ :py:class:`ModelCommand` subclass. You generally don't need to
804
+ define this, instead, you should do what you need in the
805
+ :py:meth:`Client.factory`, :py:meth:`Client.setargs` and
806
+ :py:meth:`Client.post_call` methods.
799
807
  """
800
808
  paginator = None
801
- model_command = ModelCommand
809
+ cmdclass = ModelCommand
802
810
  url_list = None
803
811
  url_detail = '{self.url_list}/{self.id_value}'
804
812
  id_field = 'id'
805
813
  cli_kwargs = dict()
806
814
 
807
- class Command(ModelCommand):
808
- pass
809
-
810
815
  def __init__(self, data=None, **values):
811
816
  """
812
817
  Instanciate a model.
@@ -984,10 +989,10 @@ class ClientMetaclass(type):
984
989
 
985
990
  cls = super().__new__(cls, name, bases, attributes)
986
991
 
987
- cls.Command = type(
992
+ cls.cmdclass = type(
988
993
  'ClientCommand',
989
- (cls.Command, Command),
990
- dict(),
994
+ (cls.cmdclass,),
995
+ dict(client_class=cls),
991
996
  )
992
997
 
993
998
  # bind ourself as _client_class to any inherited model
@@ -1005,13 +1010,15 @@ class ClientMetaclass(type):
1005
1010
  cls=dict(factory=lambda: cls),
1006
1011
  self=dict(factory=lambda: cls())
1007
1012
  ),
1008
- cmdclass=cls.Command,
1013
+ cmdclass=cls.cmdclass,
1009
1014
  )
1010
1015
  cli_kwargs.update(cls.cli_kwargs)
1011
1016
  cli = Group(**cli_kwargs)
1017
+ cli.client_class = cls
1012
1018
  cli.load(cls)
1013
1019
  for model in cls.models:
1014
1020
  group = model.cli
1021
+ group.client_class = cls
1015
1022
  if len(group) > 1:
1016
1023
  cli[model.__name__.lower()] = group
1017
1024
  cls._cli = cli
@@ -1248,6 +1255,34 @@ class FieldExternalizeError(FieldValueError):
1248
1255
  pass
1249
1256
 
1250
1257
 
1258
+ class ClientCommand(Command):
1259
+ """
1260
+ Client CLI command
1261
+
1262
+ .. py:attribute:: client
1263
+
1264
+ The client object that was constructed from :py:meth:`Client.factory`
1265
+ """
1266
+
1267
+ def setargs(self):
1268
+ """
1269
+ Set a `self` factory of :py:meth:`Client.factory` method, and call
1270
+ :py:meth:`Client.setargs`.
1271
+ """
1272
+ super().setargs()
1273
+ self['self'].factory = self.client_class.factory
1274
+ self.client_class.setargs(self)
1275
+
1276
+ async def factories_resolve(self):
1277
+ """ Set :py:attr:`client` after resolving factories. """
1278
+ await super().factories_resolve()
1279
+ self.client = self['self'].value
1280
+
1281
+ async def post_call(self):
1282
+ """ Call :py:meth:`Client.post_call`. """
1283
+ await self.client.post_call(self)
1284
+
1285
+
1251
1286
  class Client(metaclass=ClientMetaclass):
1252
1287
  """
1253
1288
  HTTPx Client Wrapper
@@ -1286,12 +1321,12 @@ class Client(metaclass=ClientMetaclass):
1286
1321
  class YourClient(cli2.Client):
1287
1322
  cli_kwargs = dict(cmdclass=YourCommandClass)
1288
1323
 
1289
- .. py:attribute:: Command
1324
+ .. py:attribute:: cmdclass
1290
1325
 
1291
- Define a Command class inside the Client class to use a specific
1292
- :py:class:`~cli2.cli.Command` class for a model.
1293
- Don't inherit from anything: a new class will be generated for you
1294
- inheriting from :py:attr:`~cli2.cli.Client.Command` for you.
1326
+ :py:class:`ClientCommand` class or subclass. You usually won't have to
1327
+ define this, instead, you should do what you need in the
1328
+ :py:meth:`factory`, :py:meth:`setargs` and :py:meth:`post_call`
1329
+ methods.
1295
1330
 
1296
1331
  .. py:attribute:: debug
1297
1332
 
@@ -1307,9 +1342,7 @@ class Client(metaclass=ClientMetaclass):
1307
1342
  semaphore = None
1308
1343
  mask = []
1309
1344
  debug = False
1310
-
1311
- class Command:
1312
- pass
1345
+ cmdclass = ClientCommand
1313
1346
 
1314
1347
  def __init__(self, *args, handler=None, semaphore=None, mask=None,
1315
1348
  debug=False, **kwargs):
@@ -1341,6 +1374,23 @@ class Client(metaclass=ClientMetaclass):
1341
1374
  model.url_list = model.url_list.format(client=self)
1342
1375
  setattr(self, model.__name__, model)
1343
1376
 
1377
+ @classmethod
1378
+ async def factory(cls):
1379
+ """
1380
+ Override this method to customize your client construction.
1381
+
1382
+ You can add custom args, if you declare them in :py:meth:`setargs()`.
1383
+ """
1384
+ return cls()
1385
+
1386
+ @classmethod
1387
+ def setargs(cls, cmd):
1388
+ """
1389
+ Override this method to declare CLI args globally for this client.
1390
+
1391
+ :param cmd: :py:class:`ClientCommand` object
1392
+ """
1393
+
1344
1394
  @property
1345
1395
  def client(self):
1346
1396
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: cli2
3
- Version: 4.1.3
3
+ Version: 4.2.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
@@ -44,7 +44,7 @@ from setuptools import setup
44
44
 
45
45
  setup(
46
46
  name='cli2',
47
- version='4.1.3',
47
+ version='4.2.0',
48
48
  setup_requires='setupmeta',
49
49
  packages=['cli2'],
50
50
  install_requires=[
@@ -60,7 +60,10 @@ async def test_client_cli(client_class, httpx_mock):
60
60
  assert 'testmodel' in client_class.cli
61
61
  assert client_class.cli['testmodel']['get'].overrides['cls']['factory']
62
62
  assert not inspect.ismethod(client_class.cli['testmodel']['get'].target)
63
- result = await client_class.cli['testmodel']['get']['cls'].factory_value()
63
+ command = client_class.cli['testmodel']['get']
64
+ command.parse()
65
+ await command.factories_resolve()
66
+ result = client_class.cli['testmodel']['get']['cls'].value
64
67
  assert issubclass(result, TestModel)
65
68
  assert isinstance(result.client, client_class)
66
69
 
@@ -148,7 +151,7 @@ async def test_client_cli_side_effect(client_class, httpx_mock):
148
151
 
149
152
  # Test that Client's __init_subclass__ did setup a factory for self
150
153
  assert isinstance(
151
- example_client.APIClient.cli['get']['self'].factory_value(),
154
+ await example_client.APIClient.cli['get']['self'].factory_value(),
152
155
  example_client.APIClient,
153
156
  )
154
157
 
@@ -1151,17 +1154,38 @@ async def test_client_token_apply(client_class, httpx_mock):
1151
1154
  assert client.client.token == 2
1152
1155
 
1153
1156
 
1154
- def test_client_command(client_class):
1157
+ def test_client_command(client_class, httpx_mock):
1155
1158
  class Client(client_class):
1156
- class Command(cli2.Command):
1157
- pass
1159
+ post_call_called = False
1160
+
1161
+ def __init__(self, base_url, *args, **kwargs):
1162
+ kwargs['base_url'] = base_url
1163
+ super().__init__(*args, **kwargs)
1164
+
1165
+ @classmethod
1166
+ async def factory(self, base_url):
1167
+ return Client(base_url)
1168
+
1169
+ @classmethod
1170
+ def setargs(self, cmd):
1171
+ cmd.arg('base_url', position=0, kind='POSITIONAL_ONLY')
1172
+
1173
+ async def post_call(self, cmd):
1174
+ await self.client.post('/logoff')
1158
1175
 
1159
1176
  class Model(Client.Model):
1160
1177
  url_list = '/foo'
1161
- class Command(Client.Command, cli2.Model.Command):
1162
- pass
1163
1178
 
1164
- assert issubclass(Client.cli.cmdclass, Client.Command)
1165
- assert issubclass(Client.cli['model'].cmdclass, Client.Command)
1166
- assert issubclass(Client.cli['model'].cmdclass, cli2.Model.Command)
1167
1179
  assert 'get' in Client.cli['model']
1180
+
1181
+ cmd = Client.cli['get']
1182
+ assert isinstance(cmd, Client.cmdclass)
1183
+ httpx_mock.add_response(url='http://x/foo', json=[])
1184
+ httpx_mock.add_response(url='http://x/logoff')
1185
+ result = cmd('http://x', '/foo')
1186
+ assert result.status_code == 200
1187
+
1188
+ httpx_mock.add_response(url='http://x/foo', json=[])
1189
+ httpx_mock.add_response(url='http://x/logoff')
1190
+ cmd = Client.cli['model']['find']
1191
+ cmd('http://x')
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes