diff --git a/CHANGELOG.md b/CHANGELOG.md index 429df7f9eb6e1ada153aa7dd843b65c1d8541715..167621184f2a2f247fd58d4a888474446a055637 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 3cb7814e9ac1c0693c45e817d2d9bc07ccc18246..4202a9e8cf38e927abde7910a708d16aee93dd31 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 e156610f5bc559e8cc711302a0a8b379ef51f6cf..64ee15dc69c1d5e26ab8bc87694585d0f31dcf83 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 a1fe7e3f7b30d3987522abd081f16aee94ae1102..a71c4d6842decbe905c5d28b775e6b89c1ff6956 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'):