Skip to content

Merge with primary schema #4

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -222,3 +222,19 @@ The resulting source code is:
continue
person_data = ctx.default_map[person_data_key]
process_person_data(person_data) # as dict.

By default the first schema listed in config_section_schemas is treated as the primary, meaning that its keys / values are merged into the top level of the default_map. This can be useful since there is then an order of precedence of variables loaded into the click command in different ways. This order is CLI > Configuration file > Environment > Default. Other schemas (not the primary), are added as a dictionary to the default_map where the key is the section name and the value is a dictionary of the keys / values in that section. These can be used manually as in the example above.

If you want to merge multiple schemas into the top level of default_map, you add them to the config_section_primary_schemas list. Understand that this will combine the namespaces, so there may be conflicts depending on usage. The advantage of this is having the precedence listed above for any listed schema. As an example:

.. code-block:: python

class ConfigFileProcessor(ConfigFileReader):
config_files = ["foo.ini", "foo.cfg"]
config_section_primary_schemas = [
ConfigSectionSchema.Foo,
ConfigSectionSchema.Bar,
]
config_section_schemas = config_section_primary_schemas + [
ConfigSectionSchema.Baz,
]
9 changes: 7 additions & 2 deletions click_configfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,7 @@ def hello(ctx, name):
config_section_schemas = [] # Config section schema description.
config_sections = [] # OPTIONAL: Config sections of interest.
config_searchpath = ["."] # OPTIONAL: Where to look for config files.
config_section_primary_schemas = [] # OPTIONAL: Schemas to merge into primary.

# -- GENERIC PART:
# Uses declarative specification from above (config_files, config_sections, ...)
Expand Down Expand Up @@ -434,7 +435,6 @@ def process_config_section(cls, config_section, storage):
# if not storage:
# # -- INIT DATA: With default parts.
# storage.update(dict(_PERSONS={}))

schema = cls.select_config_schema_for(config_section.name)
if not schema:
message = "No schema found for: section=%s"
Expand Down Expand Up @@ -472,8 +472,13 @@ def get_storage_name_for(cls, section_name):
:return: EMPTY-STRING or None, indicates MERGE-WITH-DEFAULTS.
:return: NON-EMPTY-STRING, for key in default_map to use.
"""
sections_to_merge = cls.collect_config_sections_from_schemas(
cls.config_section_primary_schemas)
if cls.config_sections and cls.config_sections[0] == section_name:
# -- PRIMARY-SECTION: Merge into storage (defaults_map).
# -- PRIMARY-SECTION: Merge into storage (default_map).
return ""
elif cls.config_sections and section_name in sections_to_merge:
# -- NON-PRIMARY-SECTION W/MERGE: Merge into storage (default_map).
return ""
else:
return section_name
Expand Down
59 changes: 59 additions & 0 deletions tests/functional/test_basics.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,30 @@ class ConfigFileProcessor3(ConfigFileReader):
]


# -----------------------------------------------------------------------------
# TEST CANDIDATE 4: With config_section_primary_schemas
# -----------------------------------------------------------------------------
@assign_param_names
class ConfigSectionSchema4(object):

@matches_section("hello4")
class Hello(SectionSchema):
first_name = Param(type=str)

@matches_section("hellomore4")
class HelloMore(SectionSchema):
last_name = Param(type=str)

class ConfigFileProcessor4(ConfigFileReader):
config_files = ["hello4.ini"]
config_section_primary_schemas = [
ConfigSectionSchema4.HelloMore,
]
config_section_schemas = [
ConfigSectionSchema4.Hello,
] + config_section_primary_schemas


# -----------------------------------------------------------------------------
# TEST SUITE
# -----------------------------------------------------------------------------
Expand Down Expand Up @@ -378,3 +402,38 @@ def test_config_searchpath__param_from_primary_file_overrides_secondary(self,
config = ConfigFileProcessor3.read_config()
assert config == dict(name="Alice")
assert config["name"] == "Alice" # -- FROM: config_file1 (prefered)

class TestCandidate4(object):

def test_configfile__use_config_section_primary_schemas(self,
cli_runner_isolated):
assert ConfigFileProcessor4.config_files[0] == "hello4.ini"
assert not os.path.exists("hello4.cfg")
CONFIG_FILE_CONTENTS = """
[hello4]
first_name = Alice

[hellomore4]
last_name = Doe
"""
write_configfile_with_contents("hello4.ini", CONFIG_FILE_CONTENTS)
assert os.path.exists("hello4.ini")

CONTEXT_SETTINGS = dict(default_map=ConfigFileProcessor4.read_config())

@click.command(context_settings=CONTEXT_SETTINGS)
@click.option("-n", "--first-name", default="__CMDLINE__")
@click.option("-n", "--last-name", default="__CMDLINE__")
@click.pass_context
def hello4(ctx, first_name, last_name):
click.echo("Hello4 first_name = %s" % first_name)
click.echo("Hello4 last_name = %s" % last_name)

assert os.path.exists("hello4.ini")
result = cli_runner_isolated.invoke(hello4)
expected_output = """\
Hello4 first_name = Alice
Hello4 last_name = Doe
"""
assert result.output == expected_output
assert result.exit_code == 0