GithubHelp home page GithubHelp logo

stephen-bunn / file-config Goto Github PK

View Code? Open in Web Editor NEW
8.0 2.0 1.0 459 KB

Attrs-like file config definitions inspired from https://github.com/hynek/environ_config

Home Page: https://pypi.org/project/file-config/

License: ISC License

Python 100.00%
python3 attrs config configuration serialization validation toml json message-pack yaml

file-config's Introduction

File Config

PyPI version Supported Versions Code Style: Black Documentation Status Codacy Grade Codecov Azure Pipelines

An attr's like interface to building class representations of config files.

from typing import List
import file_config

@file_config.config(title="My Config", description="A simple/sample config")
class MyConfig(object):

   @file_config.config(title="Config Group", description="A independent nested config")
   class Group(object):
      name = file_config.var(str)
      type = file_config.var(str, default="config")

   name = file_config.var(str, min=1, max=24)
   version = file_config.var(file_config.Regex(r"^v\d+$"))
   groups = file_config.var(List[Group], min=1)


my_config = MyConfig(
   name="Sample Config",
   version="v12",
   groups=[
      MyConfig.Group(name="Sample Group")
   ]
)

config_json = my_config.dumps_json()
# {"name":"Sample Config","version":"v12","groups":[{"name":"Sample Group","type":"config"}]}
assert my_config == ModConfig.loads_json(config_json)

Install from PyPi.

pip install file-config
# or
pipenv install file-config

Define Configs

Making config is straight-forward if you are familiar with attrs syntax. Decorate a class with the file_config.config decorator and the class is considered to be a config.

@file_config.config
class MyConfig(object):
   pass

You can check if a variable is a config type or instance by using the file_config.utils.is_config_type or file_config.utils.is_config methods.

assert file_config.utils.is_config_type(MyConfig)
assert file_config.utils.is_config(my_config)

There are two optional attributes are available on the file_config.config method (both used for validation):

  • title - Defines the title of the object in the resulting jsonschema
  • description - Defines the description of the object in the resulting jsonschema
@file_config.config(title="My Config", description="A simple/sample config")
class MyConfig(object):
   pass

Defining Config Vars

The real meat of the config class comes from adding attributes to the config through the file_config.var method. Again, if you're familiar with attrs syntax, this should be pretty straight-forward.

@file_config.config(title="My Config", description="A simple/sample config")
class MyConfig(object):

   name = file_config.var()

Required

If no args are given the the var method then the config object only expects that the variable is required when validating. You can disable the config exepecting the var to exist by setting required = False...

name = file_config.var(required=False)

Type

You can specify the type of a var by using either builtin types or most common typing types. This is accepted as either the first argument to the method or as the keyword type.

name = file_config.var(type=str)
keywords = file_config.var(type=typing.List[str])

Commonly you need to validate strings against regular expressions. Since this package is trying to stick as close as possible to Python's typing there is no builtin type to store regular expressions. To do handle this a special method was created to store regular expressions in a typing type.

version = file_config.var(type=file_config.Regex(r"^v\d+$"))

Nested configs are also possible to throw into the type keyword of the var. These are serialized into nested objects in the jsonschema.

@file_config.config
class GroupContainer(object):

   @file_config.config
   class Group(object):
      name = file_config.var(str)

   name = file_config.var(str)
   parent_group = file_config.var(Group)
   children_groups = file_config.var(typing.List[Group])

Note that types require to be json serializable. So types that don't dump out to json (like typing.Dict[int, str]) will fail in the file_config.build_schema step.

@file_config.config
class PackageConfig:
   depends = file_config.var(type=typing.Dict[int, str])
>>> file_config.build_schema(PackageConfig)
Traceback (most recent call last):
  File "main.py", line 21, in <module>
    pprint(file_config.build_schema(PackageConfig))
  File "/home/stephen-bunn/Git/file-config/file_config/schema_builder.py", line 278, in build_schema
    return _build_config(config_cls, property_path=[])
  File "/home/stephen-bunn/Git/file-config/file_config/schema_builder.py", line 261, in _build_config
    var, property_path=property_path
  File "/home/stephen-bunn/Git/file-config/file_config/schema_builder.py", line 218, in _build_var
    _build_type(var.type, var, property_path=property_path + [var.name])
  File "/home/stephen-bunn/Git/file-config/file_config/schema_builder.py", line 182, in _build_type
    return builder(value, property_path=property_path)
  File "/home/stephen-bunn/Git/file-config/file_config/schema_builder.py", line 160, in _build_object_type
    f"cannot serialize object with key of type {key_type!r}, "
ValueError: cannot serialize object with key of type <class 'int'>, located in var 'depends'

Name

The name kwarg is used for specifying the name of the variable that should be used during serialization/deserialization. This is useful for when you might need to use Python keywords as variables in your serialized configs but don't want to specify the keyword as a attribute of your config.

@file_config.config
class PackageConfig:
   type_ = file_config.var(name="type")

Title

The title kwarg of a var is used in the built jsonschema as the varaible's title.

Description

Similar to the title kwarg, the description kwarg of a var is simply used as the variable's description in the built jsonschema.

Serialization / Deserialization

To keep api's consistent, serialization and deserialization methods are dynamically added to your config class. For example, JSON serialization/deserialization is done through the following dynamically added methods:

  • dumps_json() - Returns json serialization of the config instance
  • dump_json(file_object) - Writes json serialization of the config instance to the given file object
  • loads_json(json_content) - Builds a new config instance from the given json content
  • load_json(file_object) - Builds a new config instance from the result of reading the given json file object

This changes for the different types of serialization desired. For example, when dumping toml content the method name changes from dumps_json() to dumps_toml().

By default dictionary, JSON, and Pickle serialization is included.

Dictionary

The dumping of dictionaries is a bit different than other serialization methods since a dictionary representation of a config instance is not a end result of serialization.

For this reason, representing the config instance as dictionary is done through the file_config.to_dict(config_instance) method. Loading a new config instance from a dictionary is done through the file_config.from_dict(config_class, config_dictionary) method.

>>> config_dict = file_config.to_dict(my_config)
OrderedDict([('name', 'Sample Config'), ('version', 'v12'), ('groups', [OrderedDict([('name', 'Sample Group'), ('type', 'config')])])])
>>> new_config = file_config.from_dict(MyConfig, config_dict)
MyConfig(name='Sample Config', version='v12', groups=[MyConfig.Group(name='Sample Group', type='config')])

JSON

>>> json_content = my_config.dumps_json()
{"name":"Sample Config","version":"v12","groups":[{"name":"Sample Group","type":"config"}]}
>>> new_config = MyConfig.loads_json(json_content)
MyConfig(name='Sample Config', version='v12', groups=[MyConfig.Group(name='Sample Group', type='config')])

INI

Unfortunately, INI cannot correctly serialize configs containing lists of mappings... found in the groups var. You should really be using TOML in this case, but for now INI can deal with any config that doesn't contain a list of mappings.

For example...

@file_config.config
class INIConfig(object):

   @file_config.config
   class INIConfigGroup(object):
      value = file_config.var()

   name = file_config.var(str)
   value = file_config.var(int)
   groups = file_config.var(Dict[str, INIConfigGroup])

my_config = INIConfig(
   name="My Config",
   value=-1,
   groups={"group-1": INIConfig.INIConfigGroup(value=99)}
)
>>> ini_content = my_config.dumps_ini()
[INIConfig]
name = "My Config"
value = -1
[INIConfig:groups:group-1]
value = 99
>>> new_config = INIConfig.loads_ini(ini_content)
INIConfig(name='My Config', value=-1, groups={'group-1': INIConfig.INIConfigGroup(value=99)})

Pickle

>>> pickle_content = my_config.dumps_pickle()
b'\x80\x04\x95\x7f\x00\x00\x00\x00\x00\x00\x00\x8c\x0bcollections\x94\x8c\x0bOrderedDict\x94\x93\x94)R\x94(\x8c\x04name\x94\x8c\rSample Config\x94\x8c\x07version\x94\x8c\x03v12\x94\x8c\x06groups\x94]\x94h\x02)R\x94(h\x04\x8c\x0cSample Group\x94\x8c\x04type\x94\x8c\x06config\x94uau.'
>>> new_config = MyConfig.loads_pickle(pickle_content)
MyConfig(name='Sample Config', version='v12', groups=[MyConfig.Group(name='Sample Group', type='config')])

YAML

Serializing yaml requires pyyaml, pipenv install file-config[pyyaml]

>>> yaml_content = my_config.dumps_yaml()
name: Sample Config
version: v12
groups:
   - name: Sample Group
type: config
>>> new_config = MyConfig.loads_yaml(yaml_content)
MyConfig(name='Sample Config', version='v12', groups=[MyConfig.Group(name='Sample Group', type='config')])

TOML

Serializing toml requires tomlkit, pipenv install file-config[tomlkit]

>>> toml_content = my_config.dumps_toml()
name = "Sample Config"
version = "v12"
[[groups]]
name = "Sample Group"
type = "config"
>>> new_config = MyConfig.loads_toml(toml_content)
MyConfig(name='Sample Config', version='v12', groups=[MyConfig.Group(name='Sample Group', type='config')])

Message Pack

Serializing message pack requires msgpack, pipenv install file-config[msgpack]

>>> msgpack_content = my_config.dumps_msgpack()
b'\x83\xa4name\xadSample Config\xa7version\xa3v12\xa6groups\x91\x82\xa4name\xacSample Group\xa4type\xa6config'
>>> new_config = MyConfig.loads_msgpack(msgpack_content)
MyConfig(name='Sample Config', version='v12', groups=[MyConfig.Group(name='Sample Group', type='config')])

XML

Serializing xml requires lxml, pipenv install file-config[lxml]

>>> xml_content = my_config.dumps_xml(pretty=True, xml_declaration=True)
<?xml version='1.0' encoding='UTF-8'?>
<MyConfig>
   <name type="str">Sample Config</name>
   <version type="str">v12</version>
   <groups>
      <groups>
         <name type="str">Sample Group</name>
         <type type="str">config</type>
      </groups>
   </groups>
</MyConfig>
>>> new_config = MyConfig.loads_xml(xml_content)
MyConfig(name='Sample Config', version='v12', groups=[MyConfig.Group(name='Sample Group', type='config')])

If during serialization you don't have the extra depedencies installed for the requested serialization type, a ModuleNotFoundError is raised that looks similar to the following:

>>> my_config.dumps_toml()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/stephen-bunn/.virtualenvs/tempenv-4ada15392238b/lib/python3.6/site-packages/file_config/_file_config.py", line 52, in _handle_dumps
    return handler.dumps(to_dict(self))
  File "/home/stephen-bunn/.virtualenvs/tempenv-4ada15392238b/lib/python3.6/site-packages/file_config/handlers/_common.py", line 49, in dumps
    dumps_hook_name = f"on_{self.imported}_dumps"
  File "/home/stephen-bunn/.virtualenvs/tempenv-4ada15392238b/lib/python3.6/site-packages/file_config/handlers/_common.py", line 13, in imported
    self._imported = self._discover_import()
  File "/home/stephen-bunn/.virtualenvs/tempenv-4ada15392238b/lib/python3.6/site-packages/file_config/handlers/_common.py", line 46, in _discover_import
    raise ModuleNotFoundError(f"no modules in {self.packages!r} found")
ModuleNotFoundError: no modules in ('tomlkit',) found
no modules in ('tomlkit',) found

In this case you should install tomlkit as an extra dependency using something similar to the following:

pip install file-config[tomlkit]
# or
pipenv install file-config[tomlkit]

Validation

Validation is done through jsonschema and can be used to check a config instance using the validate method.

>>> file_config.version = "v12"
>>> file_config.validate(my_config)
None
>>> my_config.version = "12"
>>> file_config.validate(mod_config)
Traceback (most recent call last):
  File "main.py", line 61, in <module>
    print(file_config.validate(my_config))
  File "/home/stephen-bunn/Git/file-config/file_config/_file_config.py", line 313, in validate
    to_dict(instance, dict_type=dict), build_schema(instance.__class__)
  File "/home/stephen-bunn/.local/share/virtualenvs/file-config-zZO-gwXq/lib/python3.6/site-packages/jsonschema/validators.py", line 823, in validate
    cls(schema, *args, **kwargs).validate(instance)
  File "/home/stephen-bunn/.local/share/virtualenvs/file-config-zZO-gwXq/lib/python3.6/site-packages/jsonschema/validators.py", line 299, in validate
    raise error
jsonschema.exceptions.ValidationError: '12' does not match '^v\\d+$'
Failed validating 'pattern' in schema['properties']['version']:
    {'$id': '#/properties/version', 'pattern': '^v\\d+$', 'type': 'string'}
On instance['version']:
    '12'

The attribute types added config vars do not imply type checking when creating an instance of the class. Attribute types are used for generating the jsonschema for the config and validating the model. This allows you to throw any data you need to throw around in the config class, but validate the config only when you need to.

You can get the jsonschema that is created to validate a config class through the build_schema method.

>>> file_config.build_schema(ModConfig)
{'$id': 'MyConfig.json',
'$schema': 'http://json-schema.org/draft-07/schema#',
'description': 'A simple/sample config',
'properties': {'groups': {'$id': '#/properties/groups',
                           'items': {'$id': '#/properties/groups/items',
                                    'description': 'A independent nested '
                                                   'config',
                                    'properties': {'name': {'$id': '#/properties/groups/items/properties/name',
                                                            'type': 'string'},
                                                   'type': {'$id': '#/properties/groups/items/properties/type',
                                                            'default': 'config',
                                                            'type': 'string'}},
                                    'required': ['name', 'type'],
                                    'title': 'Config Group',
                                    'type': 'object'},
                           'minItems': 1,
                           'type': 'array'},
               'name': {'$id': '#/properties/name',
                        'maxLength': 24,
                        'minLength': 1,
                        'type': 'string'},
               'version': {'$id': '#/properties/version',
                           'pattern': '^v\\d+$',
                           'type': 'string'}},
'required': ['name', 'version', 'groups'],
'title': 'My Config',
'type': 'object'}

file-config's People

Contributors

azazel75 avatar stephen-bunn avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

Forkers

azazel75

file-config's Issues

Testing for serializing handlers

We should have continuous testing for all of our handlers and supported modules. Hopefully this will allow us to detect if changes to those libraries will break our logic.

The kind of testing we should probably focus on is:

  • Handler is importable
  • Handler is reflective
  • Handler dumping / loading options
  • Config preferred handler keyword

Standardize type matching checks

This is a cleanup task I need to address to better standardize the logic for checking the given types of variables. This includes things like properly checking the __origin__ property of typing variables (vs. checking if the given typing var is in a list).

Essentially I need something similar to six's string_types but in an abstract way that can work for various modules such as typing, collections, and any other future things.

Fix Codacy Integration

Codacy's recent breakage caused a lot of errors to occur with the webhook process from this repo. Best thing to do is remove and re-add the project to Codacy.

So Codacy links and badges will most likely be broken for a while until they are replaced.

  • Fix Codacy badges
  • Fix Codacy testing options

Utilize defusedxml for XMLParser

Codacy raises an issue with missing defusedxml in order to prevent xml bombs. So we should add this as the default for the XMLParser which currently only depends on lxml.

I believe that defusedxml just works as a etree replacement so it shouldn't be too hard to replace some of the current logic.

  • Replace etree.fromstring with defusedxml alternative

Loading a config using typehints instead of types skips nested configs

Expected Behavior

If I either use typehints or pass in the type keyword argument to the var() definitions; the loading / dumping logic should be the same.

Current Behavior

Usage of type-hints vs. explicit type parameter is in-consistent. See the below example. In test case 1, we utilize the documented use of passing in the type to the var(). However, if the alternative attrs method of defining types (via typehints) is used (as displayed in test case 2), the loading of the config does not jump through the nested config correctly.

from typing import List
from file_config import config, var


@config
class TestCase1(object):
    @config
    class Nested(object):
        name = var(str)

    nests = var(List[Nested])


@config
class TestCase2(object):
    @config
    class Nested(object):
        name: str = var()

    nests: List[Nested] = var()


LOADABLE_JSON = """{"nests": [{"name": "hello"}, {"name": "world"}]}"""

print(TestCase1.loads_json(LOADABLE_JSON))
print(TestCase2.loads_json(LOADABLE_JSON))
TestCase1(nests=[TestCase1.Nested(name='hello'), TestCase1.Nested(name='world')])
TestCase2(nests=[{'name': 'hello'}, {'name': 'world'}])

Possible Solution

Most likely, I'm just not completely considering the available attributes in attrs._make.Attribute unless they implicitly override the type-hint value or mutate it in some way.

Steps to Reproduce (for bugs)

See the above example...

Context

Your Environment

  • File Config Version: 0.3.9
  • Python Version: 3.7
  • Operating System and Version: Windows 10 is where this bug was encountered
  • Link to your branch (if applicable):

❤️ Thank you!

Improve tests for "schema_builder" and "_file_config"

We should have better test coverage for schema_builder.py and _file_config.py as those files contain 90% of our logic. Currently we do basic testing for building basic configs and their schemas. We don't deal with edge cases for unnormal config instances.

Tests should focus on:

  • Testing all config to schema types when building schemas
  • Testing schema building for nested configs
  • Testing / validating the generated jsonschema metadata properites

Typcasting with typing._GenericAlias fails

In some use cases typing types are considered to be typing._GenericAlias subclasses instead of typing.GenericMeta. In these cases the type.__extra__ attribute is not defined as it is not considered to be part of the typing module. However the type.__origin__ attribute contains the same needed data.

(type.__args__ is the same no matter what)

This can be mitigated by trying __extra__ first (as that is the more likely scenario) and catching AttributeError in order to then try and load the base type from type.__origin__ this bug appears on https://github.com/stephen-bunn/file-config/blob/master/src/file_config/utils.py#L370.

Your Environment

  • File Config Version: 0.3.3
  • Python Version: 3.7
  • Operating System and Version: Ubuntu 18.04

❤️ Thank you!

Load into correct Python types given defined typing types

Currently the instance loading logic is very naive regarding any kind of types outside of bulitins and nested configs. Need to also determine which types to load to given a defined attribute type that is part of the typing module.

This logic should probably exist in the _build method of file_config._file_config

Default for missing nested config should be the default state of the nested config

Expected Behavior

Given the following:

from textwrap import dedent
from file_config import config, var, build_schema, utils, validate, to_dict

@config
class TestConfig:
    @config
    class InnerConfig:
        foo = var(str, default="Default", required=False)

    inner = var(InnerConfig, required=False)
    bar = var(bool, default="Default", required=False)


json_ = dedent(
    """\
    {
        "bar": false
    }
"""
)
c = TestConfig.loads_json(json_)
print(c)

I would expect the value of the parsed config instance would be TestConfig(inner=TestConfig.InnerConfig(foo="Default"), bar=False).

Current Behavior

Given the above example, the loaded TestConfig instance is TestConfig(inner=None, bar=False).

Possible Solution

The solution that would solve this case is by changing the handler for building config types in _file_config._build...

elif is_config_type(entry.type):
    if arg_key not in dictionary:
-        kwargs[var.name] = arg_default
+        kwargs[var.name] = (
+            _build(entry.type, {}) if arg_default is None else arg_default
+        )
    else:
        kwargs[var.name] = _build(
            entry.type, dictionary.get(arg_key, arg_default)
        )

@azazel75 this is related to #24. You mentioned you expect that the build config instance to be None for missing nested configs. My initial/desired implementation is to have the "default state" of the inner config to be defined.

The proposed diff would ensure this as long as default is not None for the inner = var(InnerConfig, required=False) then the nested config will be built with its "empty" state. This will cause issues if you ever truly expect the built inner config to be None.

The other solution is to internally default config vars to some kind of custom type which is interpreted like javascript's undefined.

Your Environment

  • File Config Version: 0.3.6
  • Python Version: 3.7.1
  • Operating System and Version: LUbuntu 18.10
  • Link to your branch (if applicable):

❤️ Thank you!

Editable $id and $schema properties for config generated JSONSchema

Expected Behavior

Defined config classes should have the ability to define the generated JSONschema's $id and $schema properties. Most likely the expected usage would look something like the following:

from file_config import config, var

@config(schema_id="Resource.json", schema_draft="http://json-schema.org/draft-07/schema#")
class MyResource(object):

    name: str = var()

This would build the following JSONSchema:

{
    "type": "object",
    "required":
    [
        "name"
    ],
    "properties":
    {
        "name":
        {
            "$id": "#/properties/name",
            "type": "string"
        }
    },
    "$id": "Resource.json",
    "$schema": "http://json-schema.org/draft-07/schema#"
}

Current Behavior

Currently, the default behavior to generate the id is just to take the name of the class and set that as the JSONschema $id. By default, the $id of the above config class would be MyResource.json without any method to modify this value.

The document $schema is hard-set to draft-07 as that draft is the minimum required to support several features such as pattern matching for the file_config.Regex fields. Unsure how to allow this to be mutable as of right now.

Possible Solution

The code that is utilized to set these properties is found at https://github.com/stephen-bunn/file-config/blob/master/src/file_config/schema_builder.py#L447-L451. Changing this to read properties from the config's metadata store should be simple.

I propose that we add the following optional kwargs to the config() decorator to allow the dynamic setting of generated JSONschema $id and $schema fields.

  • schema_id - controls the $id property in JSONschema
  • schema_draft - controls the $schema property in JSONschema

As we hard-depend on draft-07 for our schema building, there will probably either need to be some kind of validation or warning issued when a draft < 07 is specified for schema_draft.

Steps to Reproduce (for bugs)

N/A

Context

N/A

Your Environment

  • File Config Version: 0.3.10
  • Python Version: 3.7
  • Operating System and Version:
  • Link to your branch (if applicable):

❤️ Thank you!

defusedxml.lxml is raising deprecation warnings

Expected Behavior

defusedxml has been raising deprecation warnings as a result of it not being needed(?). However, there is currently no migration guide to switching back and no real confirmation on if defusedxml is actually no longer required.

The relevant issue is open and I'll keep on it until something comes about to switch off of defusedxml.

Current Behavior

Possible Solution

Context

Your Environment

  • File Config Version: 0.3.9
  • Python Version: 3.7

❤️ Thank you!

feature request: accept path str in load and dump methods

Expected Behavior

It would be wonderful if I can pass a path to load_json and dump_json methods, and they take care of checking/opening the file before reading/writing to it.

Current Behavior

As of now, I have to pass a file object to these methods.

I need to manually add two new methods (let's call them save and load) to take the path string, open a file for reading/writing and then call load_json or dump_json.

Schema builder skipping nested typing types when generating object types

Expected Behavior

I would expect the built schema of this config class...

from typing import Dict, List
from file_config import config, var, build_schema

@config
class Config(object):
  hooks = var(Dict[str, List[str]])

build_schema(Config)

to return

{ '$id': 'Config.json',
  '$schema': 'http://json-schema.org/draft-07/schema#',
  'properties': { 'hooks': { '$id': '#/properties/hooks',
                             'patternProperties': { '^(.*)$': { 'items': { '$id': '#/properties/hooks/items',
                                                                           'type': 'string'},
                                                                'type': 'array'}},
                             'type': 'object'}},
  'required': ['hooks'],
  'type': 'object'}

Current Behavior

Currently the generated schema comes out looking like this...

{ '$id': 'Config.json',
  '$schema': 'http://json-schema.org/draft-07/schema#',
  'properties': { 'hooks': { '$id': '#/properties/hooks',
                             'patternProperties': { '^(.*)$': {},
                             'type': 'object'}},
  'required': ['hooks'],
  'type': 'object'}

Possible Solution

This is due to the fact that schema_builder@_build is not handling typing types any differently than bulitin types. So when it tries to build the nested typing type (typing.List[str]) it is instead (somewhat silently) failing and returning an empty schema ({})

Solution would be to have a special handler in _build which deals with nested typings. This may also require some changes in the other _build_{type}_type handlers with out modifiers are built (they currently expect config vars if the given type is not a builtin).

Steps to Reproduce (for bugs)

  1. Try and run the code show in "Expected Behavior"

Your Environment

  • File Config Version: 0.3.1
  • Python Version: 3.7
  • Operating System and Version: Ubuntu 18.04

❤️ Thank you!

Loading a config from a dictionary or file does not use the specified default value

Expected Behavior

I would expect when loading a config from a dictionary, yaml, json, etc. that if a given value doesn't exist the default specified in the Config class would be used.

from file_config import config, var

@config
class TestConfig(object):
	name = var(str, default="MyName", required=False)

cfg = TestConfig.loads_json("""{}""")
assert cfg.name == "MyName"  # equals True

Current Behavior

Currently loading a config from a dictionary, yaml, json, etc. does not use the default if it doesn't exist. Instead it just sets the value to None...

from file_config import config, var

@config
class TestConfig(object):
	name = var(str, default="MyName", required=False)

cfg = TestConfig.loads_json("""{}""")
assert cfg.name == "MyConfig" # raises AssertionError
# currently cfg.name == None

Possible Solution

Luckly the solution looks pretty straight forward.
At https://github.com/stephen-bunn/file-config/blob/master/src/file_config/_file_config.py#L328

If we just replace None with arg_default the specified default value will be used. Note that arg_default is set to None if no value is given as the default (so prior implementations shouldn't break).

Your Environment

  • File Config Version: 0.3.4
  • Python Version: 3.7+
  • Operating System and Version: Ubuntu 18.10

❤️ Thank you!

XML Serialization Support

Expected Behavior

Configs should also be able to dump and load to and from XML.

Current Behavior

There should be an XMLHandler added which provides the dynamic functions dump(s)_xml and load(s)_xml to a config instance.

Possible Solution

There several libraries which go from dict to xml...

But from what I can tell, they are poorly implemented and not reflective.

TODO:

  • Add functionality for dict -> xml -> dict serialization
  • Add documentation to docs/source/handlers.rst
  • Add news about xml support

❤️ Thank you!

INI serialization support

Expected Behavior

I want to also add support for dumping to and loading from .ini files.
Should add a dumps_ini and loads_ini methods.

Current Behavior

There is no support 😞

Possible Solution

Most likely I can just use configparser.
Particularly the read_dict method will be helpful.


❤️ Thank you!

Project Restructure

I'm kind of getting tired of dealing with my default project setup. So I need to take some time and restructure the project to follow a better project structure.

Relevant changes are WIP in feature/restructure.

Add basic Sphinx documentation

I need to get some real documentation into Sphinx for readthedocs since it provides better categorization and structure.

Also I hate that GitHub and PyPi treat rst differently.

  • Setup sphinx doc structure
  • Setup user documentation sections (getting started, usage, changelog, etc...)
  • Setup automodule documentation

Config dump handler does not pass kwargs like dumps

Expected Behavior

Both dump_{x} and dumps_{x} should allow the same kwargs to be passed...

from file_config import config, var

@config
class MyConfig(object):
    name = var(str)

c = MyConfig(name="Name")
c.dumps_json(indent=2)
# or...
with open("/some/file.json", "w") as fp:
    c.dump_json(fp, indent=2)

Current Behavior

dump_{x} handlers do not accept any kwargs while dumps_{x} handlers do.

Your Environment

  • File Config Version: 0.3.2
  • Python Version: 3.7
  • Operating System and Version: Ubuntu 18.04

❤️ Thank you!

Setup testing for Windows

Add testing for Windows and Linux pipelines for multiple python versions.
Perhaps using Azure Pipelines will make this simpler to implement.

We need to support automated testing for python 3.6 and 3.7 on both Windows and Linux.

If subsections are missing the library raises an error

Expected Behavior

When a subsection of a configuration is missing from a serialized representation I would expect the deserialization to conclude without errors as it's the role of validation to check the fitting of a configuration instance.
For example, given the following configuration definition:

    @file_config.config
    class TestConfig:

        @file_config.config
        class InnerConfig:
            foo = file_config.var(str, default="Default", required=False)

        inner = file_config.var(InnerConfig, required=False)
        bar = file_config.var(str, default="Default", required=False)

and the following YAML file

bar: goofy

I expect the load() operation to complete without errors and to return an instance with bar == "goofy" and inner == None

Current Behavior

Raises a generic error trying to do None.get()

Possible Solution

Take care of the case when the subsection isn't present in the containing section dict

Your Environment

  • File Config Version: 0.3.5
  • Python Version: 3.7
  • Operating System and Version: NixOS GNU/Linux
  • Link to your branch (if applicable):

❤️ Thank you!

Preferring a serializing handler for a config disables other handlers for that config

Expected Behavior

When using the prefer keyword, I expect that the module I specify should be used for a given dump / load call. So for example:

from file_config import config, var

@config
class A:
  foo = var(str)

c = A(foo="test")
c.dumps_json(prefer="json") # will result in content dumped by the `json` module
c.dumps_json(prefer="rapidjson") # will result in content dumped by the `rapidjson` module

Current Behavior

Currently if you define a config and want to dump that config to json with two different json serialization handlers (using the prefer keyword). The first handler you prefer will be set forever and will not change on the second prefer.

For example:

from file_config import config, var

@config
class A:
	foo = var(str)

c = A(foo="test")
c.dumps_json(prefer="json") # will result in content dumped by the `json` module
c.dumps_json(prefer="rapidjson") # will result in the content dumped by the `json` (not the `rapidjson`) module

Possible Solution

Will need to take a look at how our handlers/_common.py logic for _prefer_package works. Since we try to optimize the handling of what modules get imported dynamically, we will most likely have to play around with that logic.

Your Environment

  • File Config Version: 0.3.7
  • Python Version: 3.7+
  • Operating System and Version: Linux 18.04

❤️ Thank you!

Build proper validation using file_config.Regex

Expected Behavior

@file_config.config
class ProjectConfig(object):
    name = file_config.var(str)
    dependencies = file_config.var(Dict[str, file_config.Regex(r"^v\d+$")])

config = ProjectConfig(name="My Project", dependencies={"A Dependency": "12"})
print(file_config.validate(config))

Should raise an error as 12 does not match the provided file_config.Regex (^v\d+$).

Current Behavior

Raises a warning...

/home/stephen-bunn/Git/file-config/file_config/schema_builder.py:186: UserWarning: unhandled translation for type <class 'function'>
  warnings.warn(f"unhandled translation for type {type_!r}")

But succeeds validation (when it shouldn't)

Possible Solution

The result of file_config.build_schema(ProjectConfig) is...

{'$id': 'ProjectConfig.json',
 '$schema': 'http://json-schema.org/draft-07/schema#',
 'properties': {'dependencies': {'$id': '#/properties/dependencies',
                                 'patternProperties': {'^(.*)$': {}},
                                 'type': 'object'},
                'name': {'$id': '#/properties/name', 'type': 'string'}},
 'required': ['name', 'dependencies'],
 'type': 'object'}

So it appears to be an issue with the embedding of the correct types in patternProperties for the #/properties/dependencies id.

Your Environment

  • File Config Version: 0.0.4
  • Python Version: Python 3.6
  • Operating System and Version: Linux Ubuntu 18.10
  • Link to your branch (if applicable):

❤️ Thank you!

is_regex_type raises AttributeError when evaluating typing types

Current Behavior

Building a schema for a config which mixes both file_config.Regex types and typing.List types can sometimes cause AttributeError on type_.__name__ checks for is_regex_type.

Possible Solution

This is an edge case which can be fixed by just having a statement which enforces that all types sent to is_regex_type cannot be typing types.

Steps to Reproduce (for bugs)

  1. Create a config which uses var(List[str]) and var(file_config.Regex)
  2. Edge case only occurs when Regex is evaluated before List which casues lru_cache to try and use is_regex_type again

Your Environment

  • File Config Version: 0.3.2
  • Python Version: 3.7
  • Operating System and Version: Ubuntu 18.04

❤️ Thank you!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.