From 4d5939aba85af304c8390b63058a69844cabc984 Mon Sep 17 00:00:00 2001
From: joncrall <jon.crall@kitware.com>
Date: Wed, 14 Aug 2024 19:24:33 -0400
Subject: [PATCH] Implement port_from_click

---
 CHANGELOG.md             |  5 ++
 docs/source/modules.rst  |  2 +-
 scriptconfig/__init__.py |  2 +-
 scriptconfig/config.py   | 99 +++++++++++++++++++++++++++++++---------
 4 files changed, 85 insertions(+), 23 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 429df7f..1676211 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,11 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
 * Remove 3.6 and 3.7 support
 
 
+### Fix:
+* Fixed the `define` method.
+* Initial implementation of `port_from_click`
+
+
 ## Version 0.7.15 - Released 2024-05-13
 
 ### Added
diff --git a/docs/source/modules.rst b/docs/source/modules.rst
index 3cb7814..4202a9e 100644
--- a/docs/source/modules.rst
+++ b/docs/source/modules.rst
@@ -4,4 +4,4 @@ scriptconfig
 .. toctree::
    :maxdepth: 4
 
-   scriptconfig
+   auto/scriptconfig
diff --git a/scriptconfig/__init__.py b/scriptconfig/__init__.py
index e156610..64ee15d 100644
--- a/scriptconfig/__init__.py
+++ b/scriptconfig/__init__.py
@@ -142,7 +142,7 @@ key/value pairs --- i.e. a dictionary.
 
 
 See the :mod:`scriptconfig.config` module docs for details and examples on
-getting started as well as :doc:`getting_started docs <getting_started>`
+getting started as well as :doc:`getting_started docs <manual/getting_started>`
 """
 
 __autogen__ = """
diff --git a/scriptconfig/config.py b/scriptconfig/config.py
index a1fe7e3..a71c4d6 100644
--- a/scriptconfig/config.py
+++ b/scriptconfig/config.py
@@ -142,18 +142,26 @@ def scfg_isinstance(item, cls):
 def define(default={}, name=None):
     """
     Alternate method for defining a custom Config type
+
+    Example:
+        >>> from scriptconfig.config import define, Value
+        >>> cls = define({'k1': Value('v1'), 'k2': 'v2'}, 'MyConfig')
+        >>> instance = cls()
+        >>> assert instance.to_dict() == {'k1': 'v1', 'k2': 'v2'}
+        >>> print(instance)
+        <MyConfig({'k1': 'v1', 'k2': 'v2'})>
     """
     import uuid
+    from textwrap import dedent
     if name is None:
         hashid = str(uuid.uuid4()).replace('-', '_')
         name = 'Config_{}'.format(hashid)
-    from textwrap import dedent
-    vals = {}
+    vals = {'default': default}
     code = dedent(
         '''
         import scriptconfig as scfg
         class {name}(scfg.Config):
-            pass
+            __default__ = default
         '''.strip('\n').format(name=name))
     exec(code, vals)
     cls = vals[name]
@@ -762,15 +770,15 @@ class Config(ub.NiceRepr, DictLike, metaclass=MetaConfig):
                 read_argv_kwargs['argv'] = cmdline
             self._read_argv(**read_argv_kwargs)
 
-        if 1:
-            # Check that all required variables are not the same as defaults
-            # Probably a way to make this check nicer
-            for k, v in self._default.items():
-                if scfg_isinstance(v, Value):
-                    if v.required:
-                        if self[k] == v.value:
-                            raise Exception('Required variable {!r} still has default value'.format(k))
         if not _dont_call_post_init:
+            if 1:
+                # Check that all required variables are not the same as defaults
+                # Probably a way to make this check nicer
+                for k, v in self._default.items():
+                    if scfg_isinstance(v, Value):
+                        if v.required:
+                            if self[k] == v.value:
+                                raise Exception('Required variable {!r} still has default value'.format(k))
             self.__post_init__()
         return self
 
@@ -1145,10 +1153,14 @@ class Config(ub.NiceRepr, DictLike, metaclass=MetaConfig):
             parserkw['allow_abbrev'] = self.__allow_abbrev__
         return parserkw
 
-    def port_to_dataconf(self):
+    def port_to_dataconf(self, style='dataconf'):
         """
         Helper that will write the code to express this config as a DataConfig.
 
+        TODO: In the future perhaps rename to something that indicates we can
+            write a code representation of this object in either config or data
+            config style?
+
         CommandLine:
             xdoctest -m scriptconfig.config Config.port_to_dataconf
 
@@ -1166,7 +1178,6 @@ class Config(ub.NiceRepr, DictLike, metaclass=MetaConfig):
             entries.append((key, value_kw))
         description = self._description
         name = self.__class__.__name__
-        style = 'dataconf'
         text = self._write_code(entries, name, style, description)
         return text
 
@@ -1237,21 +1248,67 @@ class Config(ub.NiceRepr, DictLike, metaclass=MetaConfig):
         return text
 
     @classmethod
-    def port_from_click(cls, click_main, name='MyConfig', style='dataconf'):
+    def port_from_click(cls, click_main, name=None, style='dataconf'):
         """
+        Prints scriptconfig code that roughtly implements some click CLI.
+
+        Args:
+            click_main (click.core.Command): command to port
+
+            name (str | None): the name of the new class, if None then
+               uses the name of the CLI command.
+
+            style (str): either dataconf or orig
+
+        Returns:
+            str : The code that roughly implements the config class.
+
+        CommandLine:
+            xdoctest -m scriptconfig.config Config.port_from_click
+
         Example:
-            @click.command()
-            @click.option('--dataset', required=True, type=click.Path(exists=True), help='input dataset')
-            @click.option('--deployed', required=True, type=click.Path(exists=True), help='weights file')
-            def click_main(dataset, deployed):
+            >>> # xdoctest: +REQUIRES(module:click)
+            >>> from scriptconfig.config import *  # NOQA
+            >>> import click
+            >>> import scriptconfig as scfg
+            >>> @click.command()
+            >>> @click.option('--dataset', required=True, type=click.Path(exists=True), help='input dataset')
+            >>> @click.option('--deployed', required=True, type=click.Path(exists=True), help='weights file')
+            >>> @click.option('--key1', default=123, type=click.Path(exists=True), help='weights file')
+            >>> @click.option('--key2', default='456', type=click.Path(exists=True), help='weights file')
+            >>> def click_main(dataset, deployed):
+            >>>     ...
+            >>> text = scfg.Config.port_from_click(click_main)
+            >>> print(text)
+            import ubelt as ub
+            import scriptconfig as scfg
+            ...
+            class click_main(scfg.DataConfig):
                 ...
+                argparse CLI generated by scriptconfig 0.8.0
+                ...
+                dataset = scfg.Value(None, required=True, help='input dataset')
+                deployed = scfg.Value(None, required=True, help='weights file')
+                key1 = scfg.Value(123, help='weights file')
+                key2 = scfg.Value(456, help='weights file')
         """
-        raise NotImplementedError('todo: figure out how to do this')
         import click
         ctx = click.Context(click.Command(''))
-        # parser = click_main.make_parser(ctx)
-        # print(f'parser={parser}')  # not an argparse object
         info_dict = click_main.to_info_dict(ctx)  # NOQA
+        default = {}
+        blocklist = {'help'}
+        for param in info_dict['params']:
+            if param['name'] in blocklist:
+                continue
+            default[param['name']] = Value(
+                param['default'],
+                required=param['required'],
+                isflag=param['is_flag'], help=param['help'])
+        if name is None:
+            name = info_dict['name'].replace('-', '_')
+        config_cls = define(default, name)
+        instance = config_cls(_dont_call_post_init=True)
+        return instance.port_to_dataconf(style=style)
 
     @classmethod
     def port_from_argparse(cls, parser, name='MyConfig', style='dataconf'):
-- 
GitLab