├── .gitignore ├── .pylintrc ├── .travis.yml ├── CHANGES.txt ├── CONTRIBUTORS.rst ├── MANIFEST.in ├── Makefile ├── README.rst ├── konfig ├── __init__.py └── tests │ ├── __init__.py │ ├── test_config.py │ └── utf8.ini ├── requirements.txt ├── setup.cfg ├── setup.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.pyc 3 | *.pyo 4 | *.egg-info 5 | *.swp 6 | build 7 | dist 8 | .tox 9 | .eggs 10 | local/ 11 | html/ 12 | .coverage 13 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; mode: conf -*- 2 | # lint Python modules using external checkers. 3 | # 4 | # This is the main checker controlling the other ones and the reports 5 | # generation. It is itself both a raw checker and an astng checker in order 6 | # to: 7 | # * handle message activation / deactivation at the module level 8 | # * handle some basic but necessary stats'data (number of classes, methods...) 9 | # 10 | [MASTER] 11 | 12 | # A comma-separated list of package or module names from where C extensions may 13 | # be loaded. Extensions are loading into the active Python interpreter and may 14 | # run arbitrary code 15 | extension-pkg-whitelist= 16 | 17 | # Add files or directories to the blacklist. They should be base names, not 18 | # paths. 19 | ignore=CVS, .git, .svn 20 | 21 | # Add files or directories matching the regex patterns to the blacklist. The 22 | # regex matches against base names, not paths. 23 | ignore-patterns= 24 | 25 | # Python code to execute, usually for sys.path manipulation such as 26 | # pygtk.require(). 27 | #init-hook= 28 | 29 | # Use multiple processes to speed up Pylint. 30 | jobs=1 31 | 32 | # List of plugins (as comma separated values of python modules names) to load, 33 | # usually to register additional checkers. 34 | load-plugins= 35 | 36 | # Pickle collected data for later comparisons. 37 | persistent=yes 38 | 39 | # Specify a configuration file. 40 | #rcfile= 41 | 42 | # Allow loading of arbitrary C extensions. Extensions are imported into the 43 | # active Python interpreter and may run arbitrary code. 44 | unsafe-load-any-extension=no 45 | 46 | 47 | [MESSAGES CONTROL] 48 | 49 | # Only show warnings with the listed confidence levels. Leave empty to show 50 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED 51 | confidence= 52 | 53 | # Disable the message, report, category or checker with the given id(s). You 54 | # can either give multiple identifiers separated by comma (,) or put this 55 | # option multiple times (only on the command line, not in the configuration 56 | # file where it should appear only once).You can also use "--disable=all" to 57 | # disable everything first and then reenable specific checks. For example, if 58 | # you want to run only the similarities checker, you can use "--disable=all 59 | # --enable=similarities". If you want to run only the classes checker, but have 60 | # no Warning level messages displayed, use"--disable=all --enable=classes 61 | # --disable=W" 62 | disable=missing-docstring,too-few-public-methods,trailing-newlines,bad-whitespace,print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,long-suffix,old-ne-operator,old-octal-literal,import-star-module-level,raw-checker-failed,bad-inline-option,locally-disabled,locally-enabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,eq-without-hash,div-method,idiv-method,rdiv-method,exception-message-attribute,invalid-str-codec,sys-max-int,bad-python3-import,deprecated-string-function,deprecated-str-translate-call 63 | 64 | # Enable the message, report, category or checker with the given id(s). You can 65 | # either give multiple identifier separated by comma (,) or put this option 66 | # multiple time (only on the command line, not in the configuration file where 67 | # it should appear only once). See also the "--disable" option for examples. 68 | enable= 69 | 70 | 71 | [REPORTS] 72 | 73 | # Python expression which should return a note less than 10 (10 is the highest 74 | # note). You have access to the variables errors warning, statement which 75 | # respectively contain the number of errors / warnings messages and the total 76 | # number of statements analyzed. This is used by the global evaluation report 77 | # (RP0004). 78 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 79 | 80 | # Template used to display messages. This is a python new-style format string 81 | # used to format the message information. See doc for all details 82 | msg-template={path}:{line}:[{msg_id}({symbol}),{obj}] {msg} 83 | 84 | # Set the output format. Available formats are text, parseable, colorized, json 85 | # and msvs (visual studio).You can also give a reporter class, eg 86 | # mypackage.mymodule.MyReporterClass. 87 | output-format=text 88 | 89 | # Tells whether to display a full report or only the messages 90 | reports=no 91 | 92 | # Activate the evaluation score. 93 | score=yes 94 | 95 | 96 | [REFACTORING] 97 | 98 | # Maximum number of nested blocks for function / method body 99 | max-nested-blocks=5 100 | 101 | 102 | [BASIC] 103 | 104 | # List of builtins function names that should not be used, separated by a comma 105 | bad-functions=map,filter,apply,input 106 | 107 | # Naming hint for argument names 108 | argument-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 109 | 110 | # Regular expression matching correct argument names 111 | argument-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 112 | 113 | # Naming hint for attribute names 114 | attr-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 115 | 116 | # Regular expression matching correct attribute names 117 | attr-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 118 | 119 | # Bad variable names which should always be refused, separated by a comma 120 | bad-names=foo,bar,baz,toto,tutu,tata 121 | 122 | # Naming hint for class attribute names 123 | class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 124 | 125 | # Regular expression matching correct class attribute names 126 | class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 127 | 128 | # Naming hint for class names 129 | class-name-hint=[A-Z_][a-zA-Z0-9]+$ 130 | 131 | # Regular expression matching correct class names 132 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 133 | 134 | # Naming hint for constant names 135 | const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 136 | 137 | # Regular expression matching correct constant names 138 | const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 139 | #const-rgx=[f]?[A-Z_][a-zA-Z0-9_]{2,30}$ 140 | 141 | # Minimum line length for functions/classes that require docstrings, shorter 142 | # ones are exempt. 143 | docstring-min-length=-1 144 | 145 | # Naming hint for function names 146 | function-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 147 | 148 | # Regular expression matching correct function names 149 | function-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 150 | 151 | # Good variable names which should always be accepted, separated by a comma 152 | good-names=v,i,j,k,ex,Run,_ 153 | 154 | # Include a hint for the correct naming format with invalid-name 155 | include-naming-hint=no 156 | 157 | # Naming hint for inline iteration names 158 | inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ 159 | 160 | # Regular expression matching correct inline iteration names 161 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 162 | 163 | # Naming hint for method names 164 | method-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 165 | 166 | # Regular expression matching correct method names 167 | method-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 168 | #method-rgx=[a-zA-Z0-9_]{2,30}$ 169 | 170 | # Naming hint for module names 171 | module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 172 | 173 | # Regular expression matching correct module names 174 | #module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 175 | module-rgx=([a-z_][a-z0-9_]*)$ 176 | 177 | # Colon-delimited sets of names that determine each other's naming style when 178 | # the name regexes allow several styles. 179 | name-group= 180 | 181 | # Regular expression which should only match function or class names that do 182 | # not require a docstring. 183 | no-docstring-rgx=^_ 184 | 185 | # List of decorators that produce properties, such as abc.abstractproperty. Add 186 | # to this list to register other decorators that produce valid properties. 187 | property-classes=abc.abstractproperty 188 | 189 | # Naming hint for variable names 190 | variable-name-hint=(([a-z][a-z0-9_]{1,30})|(_[a-z0-9_]*))$ 191 | 192 | # Regular expression matching correct variable names 193 | variable-rgx=(([a-z][a-z0-9_]{1,30})|(_[a-z0-9_]*))$ 194 | 195 | 196 | [FORMAT] 197 | 198 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 199 | expected-line-ending-format= 200 | 201 | # Regexp for a line that is allowed to be longer than the limit. 202 | ignore-long-lines=^\s*(# )??$ 203 | 204 | # Number of spaces of indent required inside a hanging or continued line. 205 | indent-after-paren=4 206 | 207 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 208 | # tab). 209 | indent-string=' ' 210 | 211 | # Maximum number of characters on a single line. 212 | max-line-length=120 213 | 214 | # Maximum number of lines in a module 215 | max-module-lines=2000 216 | 217 | # List of optional constructs for which whitespace checking is disabled. `dict- 218 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 219 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 220 | # `empty-line` allows space-only lines. 221 | no-space-check=trailing-comma,dict-separator 222 | 223 | # Allow the body of a class to be on the same line as the declaration if body 224 | # contains single statement.No config file found, using default configuration 225 | 226 | single-line-class-stmt=no 227 | 228 | # Allow the body of an if to be on the same line as the test if there is no 229 | # else. 230 | single-line-if-stmt=no 231 | 232 | 233 | [LOGGING] 234 | 235 | # Logging modules to check that the string format arguments are in logging 236 | # function parameter format 237 | logging-modules=logging 238 | 239 | 240 | [MISCELLANEOUS] 241 | 242 | # List of note tags to take in consideration, separated by a comma. 243 | notes=FIXME,XXX,TODO 244 | 245 | 246 | [SIMILARITIES] 247 | 248 | # Ignore comments when computing similarities. 249 | ignore-comments=yes 250 | 251 | # Ignore docstrings when computing similarities. 252 | ignore-docstrings=yes 253 | 254 | # Ignore imports when computing similarities. 255 | ignore-imports=no 256 | 257 | # Minimum lines number of a similarity. 258 | min-similarity-lines=4 259 | 260 | 261 | [SPELLING] 262 | 263 | # Spelling dictionary name. Available dictionaries: none. To make it working 264 | # install python-enchant package. 265 | spelling-dict= 266 | 267 | # List of comma separated words that should not be checked. 268 | spelling-ignore-words= 269 | 270 | # A path to a file that contains private dictionary; one word per line. 271 | spelling-private-dict-file= 272 | 273 | # Tells whether to store unknown words to indicated private dictionary in 274 | # --spelling-private-dict-file option instead of raising a message. 275 | spelling-store-unknown-words=no 276 | 277 | 278 | [TYPECHECK] 279 | 280 | # List of decorators that produce context managers, such as 281 | # contextlib.contextmanager. Add to this list to register other decorators that 282 | # produce valid context managers. 283 | contextmanager-decorators=contextlib.contextmanager 284 | 285 | # List of members which are set dynamically and missed by pylint inference 286 | # system, and so shouldn't trigger E1101 when accessed. Python regular 287 | # expressions are accepted. 288 | generated-members= 289 | 290 | # Tells whether missing members accessed in mixin class should be ignored. A 291 | # mixin class is detected if its name ends with "mixin" (case insensitive). 292 | ignore-mixin-members=yes 293 | 294 | # This flag controls whether pylint should warn about no-member and similar 295 | # checks whenever an opaque object is returned when inferring. The inference 296 | # can return multiple potential results while evaluating a Python object, but 297 | # some branches might not be evaluated, which results in partial inference. In 298 | # that case, it might be useful to still emit no-member and other checks for 299 | # the rest of the inferred objects. 300 | ignore-on-opaque-inference=yes 301 | 302 | # List of class names for which member attributes should not be checked (useful 303 | # for classes with dynamically set attributes). This supports the use of 304 | # qualified names. 305 | ignored-classes=optparse.Values,thread._local,_thread._local 306 | 307 | # List of module names for which member attributes should not be checked 308 | # (useful for modules/projects where namespaces are manipulated during runtime 309 | # and thus existing member attributes cannot be deduced by static analysis. It 310 | # supports qualified module names, as well as Unix pattern matching. 311 | ignored-modules= 312 | 313 | # Show a hint with possible names when a member name was not found. The aspect 314 | # of finding the hint is based on edit distance. 315 | missing-member-hint=yes 316 | 317 | # The minimum edit distance a name should have in order to be considered a 318 | # similar match for a missing member name. 319 | missing-member-hint-distance=1 320 | 321 | # The total number of similar names that should be taken in consideration when 322 | # showing a hint for a missing member. 323 | missing-member-max-choices=1 324 | 325 | 326 | [VARIABLES] 327 | 328 | # List of additional names supposed to be defined in builtins. Remember that 329 | # you should avoid to define new builtins when possible. 330 | additional-builtins= 331 | 332 | # Tells whether unused global variables should be treated as a violation. 333 | allow-global-unused-variables=yes 334 | 335 | # List of strings which can identify a callback function by name. A callback 336 | # name must start or end with one of those strings. 337 | callbacks=cb_,_cb 338 | 339 | # A regular expression matching the name of dummy variables (i.e. expectedly 340 | # not used). 341 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 342 | 343 | # Argument names that match this expression will be ignored. Default to name 344 | # with leading underscore 345 | ignored-argument-names=_.*|^ignored_|^unused_ 346 | 347 | # Tells whether we should check for unused import in __init__ files. 348 | init-import=no 349 | 350 | # List of qualified module names which can have objects that can redefine 351 | # builtins. 352 | redefining-builtins-modules=six.moves,future.builtins 353 | 354 | 355 | [CLASSES] 356 | 357 | # List of method names used to declare (i.e. assign) instance attributes. 358 | defining-attr-methods=__init__,__new__,setUp 359 | 360 | # List of member names, which should be excluded from the protected access 361 | # warning. 362 | exclude-protected=_asdict,_fields,_replace,_source,_make 363 | 364 | # List of valid names for the first argument in a class method. 365 | valid-classmethod-first-arg=cls 366 | 367 | # List of valid names for the first argument in a metaclass class method. 368 | valid-metaclass-classmethod-first-arg=mcs 369 | 370 | 371 | [DESIGN] 372 | 373 | # Maximum number of arguments for function / method 374 | max-args=8 375 | 376 | # Maximum number of attributes for a class (see R0902). 377 | max-attributes=20 378 | 379 | # Maximum number of boolean expressions in a if statement 380 | max-bool-expr=5 381 | 382 | # Maximum number of branch for function / method body 383 | max-branches=12 384 | 385 | # Maximum number of locals for function / method body 386 | max-locals=15 387 | 388 | # Maximum number of parents for a class (see R0901). 389 | max-parents=7 390 | 391 | # Maximum number of public methods for a class (see R0904). 392 | max-public-methods=20 393 | 394 | # Maximum number of return / yield for function / method body 395 | max-returns=6 396 | 397 | # Maximum number of statements in function / method body 398 | max-statements=50 399 | 400 | # Minimum number of public methods for a class (see R0903). 401 | min-public-methods=2 402 | 403 | 404 | [IMPORTS] 405 | 406 | # Allow wildcard imports from modules that define __all__. 407 | allow-wildcard-with-all=no 408 | 409 | # Analyse import fallback blocks. This can be used to support both Python 2 and 410 | # 3 compatible code, which means that the block might have code that exists 411 | # only in one or another interpreter, leading to false positives when analysed. 412 | analyse-fallback-blocks=no 413 | 414 | # Deprecated modules which should not be used, separated by a comma 415 | deprecated-modules=optparse,tkinter.tix 416 | 417 | # Create a graph of external dependencies in the given file (report RP0402 must 418 | # not be disabled) 419 | ext-import-graph= 420 | 421 | # Create a graph of every (i.e. internal and external) dependencies in the 422 | # given file (report RP0402 must not be disabled) 423 | import-graph= 424 | 425 | # Create a graph of internal dependencies in the given file (report RP0402 must 426 | # not be disabled) 427 | int-import-graph= 428 | 429 | # Force import order to recognize a module as part of the standard 430 | # compatibility libraries. 431 | known-standard-library= 432 | 433 | # Force import order to recognize a module as part of a third party library. 434 | known-third-party=enchant 435 | 436 | 437 | [EXCEPTIONS] 438 | 439 | # Exceptions that will emit a warning when being caught. Defaults to 440 | # "Exception" 441 | overgeneral-exceptions=Exception 442 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | env: 3 | - TOX_ENV=py27 4 | - TOX_ENV=py35 5 | - TOX_ENV=lint 6 | python: 3.5 7 | install: 8 | - pip install tox 9 | script: tox -e $TOX_ENV 10 | after_success: 11 | - pip install coveralls 12 | - coveralls 13 | -------------------------------------------------------------------------------- /CHANGES.txt: -------------------------------------------------------------------------------- 1 | 2.0.0 - 2016-01-17 2 | ================== 3 | 4 | * Py27, Py35 compliant 5 | 6 | 0.1 - 1.2 7 | ========= 8 | 9 | * History not yet documented / sorry 10 | -------------------------------------------------------------------------------- /CONTRIBUTORS.rst: -------------------------------------------------------------------------------- 1 | Contributors 2 | ============ 3 | 4 | Initialy created by Tarek Ziadé. 5 | 6 | Contributors in order of appearance: 7 | 8 | - Łukasz Langa 9 | - Markus Heiser 10 | 11 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGES.txt 2 | include README.rst 3 | include CONTRIBUTORS.rst 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8; mode: makefile-gmake -*- 2 | 3 | .DEFAULT = help 4 | TEST ?= . 5 | 6 | # python version to use 7 | PY ?=3 8 | # list of python packages (folders) or modules (files) of this build 9 | PYOBJECTS = konfig 10 | # folder where the python distribution takes place 11 | PYDIST ?= dist 12 | # folder where the python intermediate build files take place 13 | PYBUILD ?= build 14 | 15 | SYSTEMPYTHON = `which python$(PY) python | head -n 1` 16 | VIRTUALENV = virtualenv --python=$(SYSTEMPYTHON) 17 | VENV_OPTS = "--no-site-packages" 18 | TEST_FOLDER = ./konfig/tests 19 | 20 | ENV = ./local/py$(PY) 21 | ENV_BIN = $(ENV)/bin 22 | 23 | 24 | PHONY += help 25 | help:: 26 | @echo 'usage:' 27 | @echo 28 | @echo ' build - build virtualenv ($(ENV)) and install *developer mode*' 29 | @echo ' lint - run pylint within "build" (developer mode)' 30 | @echo ' test - run tests for all supported environments (tox)' 31 | @echo ' dist - build packages in "$(PYDIST)/"' 32 | @echo ' publish - upload "$(PYDIST)/*" files to PyPi' 33 | @echo ' clean - remove most generated files' 34 | @echo 35 | @echo 'options:' 36 | @echo 37 | @echo ' PY=3 - python version to use (default 3)' 38 | @echo ' TEST=. - choose test from $(TEST_FOLDER) (default "." runs all)' 39 | @echo 40 | @echo 'Example; a clean and fresh build (in local/py3), run all tests (py27, py35, lint)::' 41 | @echo 42 | @echo ' make clean build test' 43 | @echo 44 | 45 | 46 | PHONY += build 47 | build: $(ENV) 48 | $(ENV_BIN)/pip -v install -e . 49 | 50 | 51 | PHONY += lint 52 | lint: $(ENV) 53 | $(ENV_BIN)/pylint $(PYOBJECTS) --rcfile ./.pylintrc 54 | 55 | PHONY += test 56 | test: $(ENV) 57 | $(ENV_BIN)/tox -vv 58 | 59 | $(ENV): 60 | $(VIRTUALENV) $(VENV_OPTS) $(ENV) 61 | $(ENV_BIN)/pip install -r requirements.txt 62 | 63 | # for distribution, use python from virtualenv 64 | PHONY += dist 65 | dist: clean-dist $(ENV) 66 | $(ENV_BIN)/python setup.py \ 67 | sdist -d $(PYDIST) \ 68 | bdist_wheel --bdist-dir $(PYBUILD) -d $(PYDIST) 69 | 70 | PHONY += publish 71 | publish: dist 72 | $(ENV_BIN)/twine upload $(PYDIST)/* 73 | 74 | PHONY += clean-dist 75 | clean-dist: 76 | rm -rf ./$(PYBUILD) ./$(PYDIST) 77 | 78 | 79 | PHONY += clean 80 | clean: clean-dist 81 | rm -rf ./local ./.cache 82 | rm -rf *.egg-info .coverage 83 | rm -rf .eggs .tox html 84 | find . -name '*~' -exec echo rm -f {} + 85 | find . -name '*.pyc' -exec rm -f {} + 86 | find . -name '*.pyo' -exec rm -f {} + 87 | find . -name __pycache__ -exec rm -rf {} + 88 | 89 | # END of Makefile 90 | .PHONY: $(PHONY) 91 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ====== 2 | Konfig 3 | ====== 4 | 5 | Yet another configuration object. Compatible with the updated `configparser 6 | `_. 7 | 8 | |travis| |master-coverage| 9 | 10 | 11 | .. |master-coverage| image:: 12 | https://coveralls.io/repos/mozilla-services/konfig/badge.svg?branch=master 13 | :alt: Coverage 14 | :target: https://coveralls.io/r/mozilla-services/konfig 15 | 16 | .. |travis| image:: https://travis-ci.org/mozilla-services/konfig.svg?branch=master 17 | :target: https://travis-ci.org/mozilla-services/konfig 18 | 19 | 20 | Usage 21 | ===== 22 | 23 | .. code-block:: python 24 | 25 | >>> from konfig import Config 26 | >>> c = Config('myconfig.ini') 27 | 28 | 29 | Then read `configparser's documentation 30 | `_ for the APIs. 31 | 32 | Konfig adds some extra APIs like **as_args()**, which will return 33 | the config file as argparse compatible arguments:: 34 | 35 | >>> c.as_args() 36 | ['--other-stuff', '10', '--httpd', '--statsd-endpoint', 'http://ok'] 37 | 38 | 39 | For automatic filtering, you can also pass an argparse parser object 40 | to **scan_args()**. I will iterate over the arguments you've defined in the 41 | parser and look for them in the config file, then return a list of args 42 | like **as_args()**. You can then use this list directly 43 | with **parser.parse_args()** - or complete it with sys.argv or whatever. 44 | 45 | >>> import argparse 46 | >>> parser = argparse.ArgumentParser() 47 | >>> parser.add_argument('--log-level', dest='loglevel') 48 | >>> parser.add_argument('--log-output', dest='logoutput') 49 | >>> parser.add_argument('--daemon', dest='daemonize', action='store_true') 50 | 51 | >>> config = Config('myconfig.ini') 52 | >>> args_from_config = config.scan_args(parser) 53 | 54 | >>> parser.parse_args(args=sys.argv[1:]+args_from_config) 55 | 56 | 57 | Syntax Definition 58 | ================= 59 | 60 | The configuration file is a ini-based file. (See 61 | http://en.wikipedia.org/wiki/INI_file for more details.) Variables name can be 62 | assigned values, and grouped into sections. A line that starts with "#" is 63 | commented out. Empty lines are also removed. 64 | 65 | Example: 66 | 67 | .. code-block:: ini 68 | 69 | [section1] 70 | # comment 71 | name = value 72 | name2 = "other value" 73 | 74 | [section2] 75 | foo = bar 76 | 77 | Ini readers in Python, PHP and other languages understand this syntax. 78 | Although, there are subtle differences in the way they interpret values and in 79 | particular if/how they convert them. 80 | 81 | Values conversion 82 | ================= 83 | 84 | Here are a set of rules for converting values: 85 | 86 | - If value is quoted with " chars, it's a string. This notation is useful to 87 | include "=" characters in the value. In case the value contains a " character, 88 | it must be escaped with a "\" character. 89 | 90 | - When the value is composed of digits and optionally prefixed by "-", it's 91 | tentatively converted to an integer or a long depending on the language. If the 92 | number exceeds the range available in the language, it's left as a string. 93 | 94 | - If the value is "true" or "false", it's converted to a boolean, or 0 and 1 95 | when the language does not have a boolean type. 96 | 97 | - A value can be an environment variable : "${VAR}" is replaced by the value of 98 | VAR if found in the environment. If the variable is not found, an error must be 99 | raised. 100 | 101 | - A value can contains multiple lines. When read, lines are converted into a 102 | sequence of values. Each new line for a multiple lines value must start with a 103 | least one space or tab character. 104 | 105 | Examples: 106 | 107 | .. code-block:: ini 108 | 109 | [section1] 110 | # comment 111 | a_flag = True 112 | a_number = 1 113 | a_string = "other=value" 114 | another_string = other value 115 | a_list = one 116 | two 117 | three 118 | user = ${USERNAME} 119 | 120 | 121 | Extending a file 122 | ================ 123 | 124 | An INI file can extend another file. For this, a "DEFAULT" section must contain 125 | an "extends" variable that can point to one or several INI files which will be 126 | merged to the current file by adding new sections and values. 127 | 128 | If the file pointed in "extends" contains section/variable names that already 129 | exist in the original file, they will not override existing ones. 130 | 131 | Here's an example: you have a public config file and want to keep some database 132 | passwords private. You can have those password in a separate file. 133 | 134 | public.ini: 135 | 136 | .. code-block:: ini 137 | 138 | [database] 139 | user = tarek 140 | password = PUBLIC 141 | 142 | [section2] 143 | foo = baz 144 | bas = bar 145 | 146 | 147 | And then in private.ini: 148 | 149 | .. code-block:: ini 150 | 151 | [DEFAULT] 152 | extends = public.ini 153 | 154 | [database] 155 | password = secret 156 | 157 | Now if you use *private.ini* you will get: 158 | 159 | .. code-block:: ini 160 | 161 | [database] 162 | user = tarek 163 | password = secret 164 | 165 | [section2] 166 | foo = baz 167 | bas = bar 168 | 169 | 170 | 171 | To point several files, the multi-line notation can be used: 172 | 173 | .. code-block:: ini 174 | 175 | [DEFAULT] 176 | extends = public1.ini 177 | public2.ini 178 | 179 | 180 | When several files are provided, they are processed sequentially. So if the 181 | first one has a value that is also present in the second, the second one will 182 | be ignored. This means that the configuration goes from the most specialized to 183 | the most common. 184 | 185 | Override mode 186 | ============= 187 | 188 | If you want to extend a file and have existing values overridden, 189 | you can use "overrides" instead of "extends". 190 | 191 | Here's an example. file2.ini: 192 | 193 | .. code-block:: ini 194 | 195 | [section1] 196 | name2 = "other value" 197 | 198 | [section2] 199 | foo = baz 200 | bas = bar 201 | 202 | 203 | file1.ini: 204 | 205 | .. code-block:: ini 206 | 207 | [DEFAULT] 208 | overrides = file2.ini 209 | 210 | [section2] 211 | foo = bar 212 | 213 | 214 | Result if you use *file1.ini*: 215 | 216 | .. code-block:: ini 217 | 218 | [section1] 219 | name2 = "other value" 220 | 221 | [section2] 222 | foo = baz 223 | bas = bar 224 | 225 | In *section2*, notice that *foo* is now *baz*. 226 | 227 | -------------------------------------------------------------------------------- /konfig/__init__.py: -------------------------------------------------------------------------------- 1 | # ***** BEGIN LICENSE BLOCK ***** 2 | # This Source Code Form is subject to the terms of the Mozilla Public 3 | # License, v. 2.0. If a copy of the MPL was not distributed with this 4 | # file, 5 | # You can obtain one at http://mozilla.org/MPL/2.0/. 6 | # ***** END LICENSE BLOCK ***** 7 | """ Configuration file reader / writer 8 | """ 9 | 10 | __version__ = "2.0.0rc1" 11 | __description__ = "Yet Another Config Parser." 12 | __url__ = "https://github.com/mozilla-services/konfig" 13 | __license__ = "MPLv2.0" 14 | __author__ = 'Tarek Ziade' 15 | __author_email__ = 'tarek@mozilla.com' 16 | __keywords__ = 'config konfig configparser' 17 | 18 | 19 | import re 20 | import os 21 | from configparser import ConfigParser, ExtendedInterpolation 22 | from six import string_types, integer_types 23 | 24 | 25 | _IS_NUMBER = re.compile('^-?[0-9]+$') 26 | _IS_FLOAT = re.compile(r'^-?[0-9]+\.[0-9]*$|^-?\.[0-9]+$') 27 | 28 | 29 | class ExtendedEnvironmentInterpolation(ExtendedInterpolation): 30 | def __init__(self): 31 | items = os.environ.items() 32 | self.environment = dict((k, v.replace('$', '$$')) for k, v in items) 33 | self.klass = super(ExtendedEnvironmentInterpolation, self) 34 | 35 | def before_get(self, parser, section, option, value, defaults): 36 | defaults = self.environment 37 | defaults['HERE'] = '$${HERE}' 38 | if parser.filename: 39 | defaults['HERE'] = os.path.dirname(parser.filename) 40 | 41 | if defaults['HERE'] == '': 42 | defaults['HERE'] = os.curdir 43 | 44 | result = self.klass.before_get(parser, section, option, value, 45 | defaults) 46 | if '\n' in result: 47 | return [line for line in [self._unserialize(line) 48 | for line in result.split('\n')] 49 | if line != ''] 50 | return self._unserialize(result) 51 | 52 | def before_set(self, parser, section, option, value): 53 | result = self.klass.before_set(parser, section, option, value) 54 | return self._serialize(result) 55 | 56 | def _serialize(self, value): # pylint: disable=no-self-use 57 | if isinstance(value, bool): 58 | value = str(value).lower() 59 | elif isinstance(value, integer_types): 60 | value = str(value) 61 | elif isinstance(value, (list, tuple)): 62 | value = '\n'.join([' %s' % line for line in value]).strip() 63 | else: 64 | value = str(value) 65 | return value 66 | 67 | def _unserialize(self, value): # pylint: disable=no-self-use 68 | if not isinstance(value, string_types): 69 | # already converted 70 | return value 71 | 72 | value = value.strip() 73 | if _IS_NUMBER.match(value): 74 | return int(value) 75 | elif _IS_FLOAT.match(value): 76 | return float(value) 77 | elif value.startswith('"') and value.endswith('"'): 78 | return value[1:-1] 79 | elif value.lower() in ('true', 'false'): 80 | return value.lower() == 'true' 81 | return value 82 | 83 | 84 | class Config(ConfigParser): # pylint: disable=too-many-ancestors 85 | 86 | def __init__(self, filename): 87 | # let's read the file 88 | ConfigParser.__init__(self, **self._configparser_kwargs()) 89 | if isinstance(filename, string_types): 90 | self.filename = filename 91 | self.read(filename) 92 | else: 93 | self.filename = None 94 | self.read_file(filename) 95 | 96 | def optionxform(self, option): # pylint: disable=method-hidden, arguments-differ 97 | return option 98 | 99 | def _read(self, fp, filename): # pylint: disable=arguments-differ 100 | # first pass 101 | ConfigParser._read(self, fp, filename) 102 | 103 | # let's expand it now if needed 104 | defaults = self.defaults() 105 | 106 | def _list(name): 107 | if name not in defaults: 108 | return [] 109 | value = defaults[name] 110 | if not isinstance(value, list): 111 | value = [value] 112 | return value 113 | 114 | if 'extends' in defaults or 'overrides' in defaults: 115 | interpolate = self._interpolation.before_get 116 | 117 | for file_ in _list('extends'): 118 | file_ = interpolate(self, 'defaults', 'extends', file_, {}) 119 | self._extend(file_) 120 | 121 | for file_ in _list('overrides'): 122 | file_ = interpolate(self, 'defaults', 'overrides', file_, {}) 123 | self._extend(file_, override=True) 124 | 125 | def get_map(self, section=None): 126 | """returns a dict representing the config set""" 127 | if section: 128 | return dict(self.items(section)) 129 | 130 | res = {} 131 | for section in self: # pylint: disable=redefined-argument-from-local 132 | for option, value in self[section].items(): 133 | option = '%s.%s' % (section, option) 134 | res[option] = value 135 | return res 136 | 137 | def mget(self, section, option): 138 | value = self.get(section, option) 139 | if not isinstance(value, list): 140 | return [value] 141 | return value 142 | 143 | def _extend(self, filename, override=False): 144 | """Expand the config with another file.""" 145 | if not os.path.isfile(filename): 146 | raise IOError('No such file: %s' % filename) 147 | parser = ConfigParser(**self._configparser_kwargs()) 148 | parser.optionxform = lambda option: option 149 | parser.filename = filename 150 | parser.read([filename]) 151 | # pylint: disable=E1101,W0212 152 | serialize = self._interpolation._serialize 153 | 154 | for section in parser: 155 | if section in self: 156 | for option in parser[section]: 157 | if option not in self[section] or override: 158 | value = parser[section][option] 159 | self[section][option] = serialize(value) 160 | else: 161 | self[section] = parser[section] 162 | 163 | def _configparser_kwargs(self): # pylint: disable=no-self-use 164 | return { 165 | 'interpolation': ExtendedEnvironmentInterpolation(), 166 | 'comment_prefixes': ('#',), 167 | } 168 | 169 | def scan_args(self, parser, strip_prefixes=None): 170 | args = [] 171 | 172 | # for each option in the parser we look for it in the config 173 | prefixes = ['DEFAULT'] 174 | if strip_prefixes is not None: 175 | prefixes.extend(strip_prefixes) 176 | 177 | # building the list we have 178 | scanned = {} 179 | for key, value in self.get_map().items(): 180 | # type conversion 181 | if isinstance(value, (list, tuple)): 182 | value = [str(v) for v in value] 183 | 184 | scanned[self._convert_key(key, prefixes)] = value 185 | 186 | # now trying to see if we have matches 187 | args = [] 188 | 189 | for action in parser._actions: # pylint: disable=W0212 190 | option = action.option_strings 191 | if '--help' in option or option == []: 192 | continue 193 | 194 | option = option[-1] 195 | if option in scanned: 196 | value = scanned[option] 197 | if not isinstance(value, list): 198 | value = [value] 199 | 200 | for v in value: 201 | # regular option 202 | args.append(option) 203 | if not isinstance(v, bool): 204 | args.append(str(v)) 205 | 206 | return args 207 | 208 | def _convert_key(self, key, prefixes=None): # pylint: disable=no-self-use 209 | if prefixes is None: 210 | prefixes = [] 211 | 212 | for prefix in prefixes: 213 | if key.startswith('%s.' % prefix): 214 | key = key[len('%s.' % prefix):] 215 | break 216 | 217 | key = key.replace('.', '-') 218 | key = key.replace('_', '-') 219 | return '--' + key 220 | 221 | def as_args(self, strip_prefixes=None, omit_sections=None, 222 | omit_options=None): 223 | """Returns a list that can be passed to argparse or optparse. 224 | 225 | Each section/option is turned into "--section-option value" 226 | 227 | If the value is a boolean, the value will be omited. 228 | If the value is a sequence, it will be converted to a comma 229 | separated list 230 | 231 | Options: 232 | 233 | * strip_prefixes: a list of section names that will be stripped 234 | so the argument coverted as --option instead of --section-option 235 | 236 | * omit_sections: a list of section to ignore 237 | 238 | * omit_options: a list of options to ignore. Each option 239 | is provided as a 2-tuple (section, option) 240 | """ 241 | args = [] 242 | 243 | prefixes = ['DEFAULT'] 244 | if strip_prefixes is not None: 245 | prefixes.extend(strip_prefixes) 246 | 247 | if omit_sections is None: 248 | omit_sections = [] 249 | 250 | if omit_options is None: 251 | omit_options = [] 252 | 253 | omit_options = ['%s.%s' % (sec, option) 254 | for sec, option in omit_options] 255 | 256 | def _omit(key): 257 | if key in omit_options: 258 | return True 259 | 260 | for sec in omit_sections: 261 | if key.startswith('%s.' % sec): 262 | return True 263 | return False 264 | 265 | for key, value in self.get_map().items(): 266 | if _omit(key): 267 | continue 268 | 269 | args.append(self._convert_key(key, prefixes)) 270 | 271 | # type conversion 272 | if isinstance(value, bool): 273 | continue 274 | elif isinstance(value, (list, tuple)): 275 | value = ','.join([str(v) for v in value]) 276 | 277 | args.append(str(value)) 278 | 279 | return args 280 | 281 | 282 | class SettingsDict(dict): 283 | """A dict subclass with some extra helpers for dealing with app settings. 284 | 285 | This class extends the standard dictionary interface with some extra helper 286 | methods that are handy when dealing with application settings. It expects 287 | the keys to be dotted setting names, where each component indicates one 288 | section in the settings heirarchy. You get the following extras: 289 | 290 | * setdefaults: copy any unset settings from another dict 291 | * getsection: return a dict of settings for just one subsection 292 | 293 | """ 294 | 295 | separator = "." 296 | 297 | def copy(self): 298 | """D.copy() -> a shallow copy of D. 299 | 300 | This overrides the default dict.copy method to ensure that the 301 | copy is also an instance of SettingsDict. 302 | """ 303 | new_items = self.__class__() 304 | for k, v in self.items(): 305 | new_items[k] = v 306 | return new_items 307 | 308 | def getsection(self, section): 309 | """Get a dict for just one sub-section of the config. 310 | 311 | This method extracts all the keys belonging to the name section and 312 | returns those values in a dict. The section name is removed from 313 | each key. For example:: 314 | 315 | >>> c = SettingsDict({"a.one": 1, "a.two": 2, "b.three": 3}) 316 | >>> c.getsection("a") 317 | {"one": 1, "two", 2} 318 | >>> 319 | >>> c.getsection("b") 320 | {"three": 3} 321 | >>> 322 | >>> c.getsection("c") 323 | {} 324 | 325 | """ 326 | section_items = self.__class__() 327 | # If the section is "" then get keys without a section. 328 | if not section: 329 | for key, value in self.items(): 330 | if self.separator not in key: 331 | section_items[key] = value 332 | # Otherwise, get keys prefixed with that section name. 333 | else: 334 | prefix = section + self.separator 335 | for key, value in self.items(): 336 | if key.startswith(prefix): 337 | section_items[key[len(prefix):]] = value 338 | return section_items 339 | 340 | def setdefaults(self, *args, **kwds): 341 | """Import unset keys from another dict. 342 | 343 | This method lets you update the dict using defaults from another 344 | dict and/or using keyword arguments. It's like the standard update() 345 | method except that it doesn't overwrite existing keys. 346 | """ 347 | for arg in args: 348 | if hasattr(arg, "keys"): 349 | for k in arg: 350 | self.setdefault(k, arg[k]) 351 | else: 352 | for k, v in arg: 353 | self.setdefault(k, v) 354 | for k, v in kwds.items(): 355 | self.setdefault(k, v) 356 | -------------------------------------------------------------------------------- /konfig/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 | # You can obtain one at http://mozilla.org/MPL/2.0/. 4 | -------------------------------------------------------------------------------- /konfig/tests/test_config.py: -------------------------------------------------------------------------------- 1 | # coding: utf8 2 | # ***** BEGIN LICENSE BLOCK ***** 3 | # This Source Code Form is subject to the terms of the Mozilla Public 4 | # License, v. 2.0. If a copy of the MPL was not distributed with this 5 | # file, 6 | # You can obtain one at http://mozilla.org/MPL/2.0/. 7 | # ***** END LICENSE BLOCK ***** 8 | import argparse 9 | import unittest 10 | import tempfile 11 | import os 12 | from six import StringIO 13 | 14 | from konfig import Config, SettingsDict 15 | 16 | 17 | _FILE_ONE = """\ 18 | [DEFAULT] 19 | extends = ${TEMPFILE} 20 | 21 | [one] 22 | foo = bar 23 | num = -12 24 | not_a_num = 12abc 25 | st = "o=k" 26 | lines = 1 27 | two 28 | 3 29 | 30 | env = some ${__STUFF__} 31 | location = ${HERE} 32 | 33 | [two] 34 | a = b 35 | """ 36 | 37 | _FILE_TWO = """\ 38 | [one] 39 | foo = baz 40 | bee = 1 41 | two = "a" 42 | 43 | [three] 44 | more = stuff 45 | location = ${HERE} 46 | """ 47 | 48 | _FILE_THREE = """\ 49 | [DEFAULT] 50 | extends = no-no,no-no-no-no,no-no-no-no,theresnolimit 51 | 52 | [one] 53 | foo = bar 54 | """ 55 | 56 | _FILE_FOUR = """\ 57 | [global] 58 | foo = bar 59 | baz = bawlp 60 | 61 | [auth] 62 | a = b 63 | c = d 64 | 65 | [storage] 66 | e = f 67 | g = h 68 | 69 | [multi:once] 70 | storage.i = j 71 | storage.k = l 72 | 73 | [multi:thrice] 74 | storage.i = jjj 75 | storage.k = lll 76 | """ 77 | 78 | _EXTRA = """\ 79 | [some] 80 | stuff = True 81 | 82 | [other] 83 | thing = ok 84 | """ 85 | 86 | 87 | _FILE_OVERRIDE = """\ 88 | [DEFAULT] 89 | overrides = ${TEMPFILE} 90 | 91 | [one] 92 | foo = bar 93 | """ 94 | 95 | _FILE_ARGS = """\ 96 | [circus] 97 | httpd = True 98 | zmq_endpoint = http://ok 99 | 100 | [other] 101 | stuff = 10.3 102 | thing = bleh 103 | 104 | [floats] 105 | stuff = 10.3 106 | float = 9. 107 | again = .3 108 | digits = 10.34 109 | digits2 = .34 110 | 111 | [bleh] 112 | mew = 10 113 | 114 | [mi] 115 | log_level = DEBUG 116 | log_output = stdout 117 | daemon = True 118 | pidfile = pid 119 | multi = one 120 | two 121 | three 122 | """ 123 | 124 | 125 | class ConfigTestCase(unittest.TestCase): 126 | 127 | def setUp(self): 128 | os.environ['__STUFF__'] = 'stuff' 129 | fp, filename = tempfile.mkstemp() 130 | f = os.fdopen(fp, 'w') 131 | f.write(_FILE_TWO) 132 | f.close() 133 | os.environ['TEMPFILE'] = filename 134 | self.file_one = StringIO(_FILE_ONE) 135 | self.file_two = filename 136 | self.file_three = StringIO(_FILE_THREE) 137 | self.file_override = StringIO(_FILE_OVERRIDE) 138 | self.file_args = StringIO(_FILE_ARGS) 139 | fp, filename = tempfile.mkstemp() 140 | f = os.fdopen(fp, 'w') 141 | f.write(_FILE_FOUR) 142 | f.close() 143 | self.file_four = filename 144 | 145 | def tearDown(self): 146 | if '__STUFF__' in os.environ: 147 | del os.environ['__STUFF__'] 148 | os.remove(self.file_two) 149 | 150 | def test_reader(self): 151 | config = Config(self.file_one) 152 | 153 | # values conversion 154 | self.assertMultiLineEqual(config.get('one', 'foo'), 'bar') 155 | self.assertTrue(config.get('one', 'num') == -12) 156 | self.assertMultiLineEqual(config.get('one', 'not_a_num'), "12abc") 157 | self.assertMultiLineEqual(config.get('one', 'st'), 'o=k') 158 | self.assertListEqual(config.get('one', 'lines'), [1, 'two', 3]) 159 | self.assertMultiLineEqual(config.get('one', 'env'), 'some stuff') 160 | 161 | # getting a map 162 | _map = config.get_map() 163 | self.assertMultiLineEqual(_map['one.foo'], 'bar') 164 | 165 | _map = config.get_map('one') 166 | self.assertMultiLineEqual(_map['foo'], 'bar') 167 | 168 | del os.environ['__STUFF__'] 169 | self.assertMultiLineEqual(config.get('one', 'env'), 'some stuff') 170 | 171 | # extends 172 | self.assertMultiLineEqual(config.get('three', 'more'), 'stuff') 173 | self.assertMultiLineEqual(config.get('one', 'two'), 'a') 174 | 175 | def test_nofile(self): 176 | # if a user tries to use an inexistant file in extensions, 177 | # pops an error 178 | self.assertRaises(IOError, Config, self.file_three) 179 | 180 | def test_settings_dict_copy(self): 181 | settings = SettingsDict({"a.one": 1, 182 | "a.two": 2, 183 | "b.three": 3, 184 | "four": 4}) 185 | new_settings = settings.copy() 186 | self.assertEqual(settings, new_settings) 187 | self.assertTrue(isinstance(new_settings, SettingsDict)) 188 | 189 | def test_settings_dict_getsection(self): 190 | settings = SettingsDict({"a.one": 1, 191 | "a.two": 2, 192 | "b.three": 3, 193 | "four": 4}) 194 | 195 | self.assertDictEqual(settings.getsection("a"), {"one": 1, "two": 2}) 196 | self.assertDictEqual(settings.getsection("b"), {"three": 3}) 197 | self.assertDictEqual(settings.getsection("c"), {}) 198 | self.assertDictEqual(settings.getsection(""), {"four": 4}) 199 | 200 | def test_settings_dict_setdefaults(self): 201 | settings = SettingsDict({"a.one": 1, 202 | "a.two": 2, 203 | "b.three": 3, 204 | "four": 4}) 205 | 206 | settings.setdefaults({"a.two": "TWO", "a.five": 5, "new": "key"}) 207 | self.assertDictEqual(settings.getsection("a"), 208 | {"one": 1, "two": 2, "five": 5}) 209 | self.assertDictEqual(settings.getsection("b"), {"three": 3}) 210 | self.assertDictEqual(settings.getsection("c"), {}) 211 | self.assertDictEqual( 212 | settings.getsection(""), {"four": 4, "new": "key"}) 213 | 214 | def test_location_interpolation(self): 215 | config = Config(self.file_one) 216 | # file_one is a StringIO, so it has no location. 217 | self.assertMultiLineEqual(config.get('one', 'location'), '${HERE}') 218 | # file_two is a real file, so it has a location. 219 | file_two_loc = os.path.dirname(self.file_two) 220 | self.assertMultiLineEqual( 221 | config.get('three', 'location'), file_two_loc) 222 | 223 | def test_override_mode(self): 224 | config = Config(self.file_override) 225 | self.assertMultiLineEqual(config.get('one', 'foo'), 'baz') 226 | self.assertMultiLineEqual(config.get('three', 'more'), 'stuff') 227 | 228 | def test_convert_float(self): 229 | config = Config(self.file_args) 230 | self.assertEqual(config['floats']['stuff'], 10.3) 231 | self.assertEqual(config['floats']['float'], 9.0) 232 | self.assertEqual(config['floats']['again'], .3) 233 | self.assertEqual(config['floats']['digits'], 10.34) 234 | self.assertEqual(config['floats']['digits2'], .34) 235 | 236 | def test_as_args(self): 237 | config = Config(self.file_args) 238 | args = config.as_args(strip_prefixes=['circus'], 239 | omit_sections=['bleh', 'mi', 'floats'], 240 | omit_options=[('other', 'thing')]) 241 | 242 | wanted = ['--other-stuff', '10.3', '--httpd', 243 | '--zmq-endpoint', 'http://ok'] 244 | wanted.sort() 245 | args.sort() 246 | self.assertEqual(args, wanted) 247 | 248 | args = config.as_args(omit_sections=['bleh', 'mi', 'floats']) 249 | wanted = ['--circus-zmq-endpoint', 'http://ok', '--other-thing', 250 | 'bleh', '--other-stuff', '10.3', '--circus-httpd'] 251 | wanted.sort() 252 | args.sort() 253 | self.assertEqual(args, wanted) 254 | 255 | # it also works with an argparse parser 256 | parser = argparse.ArgumentParser(description='Run some watchers.') 257 | parser.add_argument('config', help='configuration file', nargs='?') 258 | 259 | parser.add_argument('-L', '--log-level', dest='loglevel') 260 | parser.add_argument('--log-output', dest='logoutput') 261 | parser.add_argument('--daemon', dest='daemonize', action='store_true') 262 | parser.add_argument('--pidfile', dest='pidfile') 263 | parser.add_argument('--multi', action='append') 264 | 265 | args = config.scan_args(parser, strip_prefixes=['mi']) 266 | args.sort() 267 | 268 | wanted = ['--log-level', u'DEBUG', '--log-output', u'stdout', 269 | '--daemon', '--pidfile', u'pid', '--multi', 270 | 'one', '--multi', 'two', '--multi', 'three'] 271 | wanted.sort() 272 | 273 | self.assertEqual(wanted, args) 274 | 275 | def test_utf8(self): 276 | utf8 = os.path.join(os.path.dirname(__file__), 'utf8.ini') 277 | config = Config(utf8) 278 | self.assertEqual(config.get('ok', 'yeah'), u'é') 279 | -------------------------------------------------------------------------------- /konfig/tests/utf8.ini: -------------------------------------------------------------------------------- 1 | # comments with é 2 | [ok] 3 | yeah = é 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # requires 2 | configparser 3 | six 4 | 5 | # develop 6 | pytest 7 | pytest-cov 8 | pip 9 | pylint 10 | tox 11 | twine 12 | wheel 13 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | # This flag says that the code is written to work on both Python 2 and Python 3 | # 3. If at all possible, it is good practice to do this. If you cannot, you 4 | # will need to generate wheels for each Python version that you support. 5 | universal=1 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; mode: python -*- 3 | 4 | import re 5 | import codecs 6 | from setuptools import setup, find_packages 7 | 8 | def read_file(fname): 9 | with codecs.open(fname, 'r', 'utf-8') as f: 10 | return f.read() 11 | 12 | def find_meta(meta): 13 | """ 14 | Extract __*meta*__ from META_FILE. 15 | """ 16 | meta_match = re.search( 17 | r"^__{meta}__\s*=\s*['\"]([^'\"]*)['\"]".format(meta=meta), 18 | META_FILE 19 | , re.M ) 20 | if meta_match: 21 | return meta_match.group(1) 22 | raise RuntimeError("Unable to find __{meta}__ string.".format(meta=meta)) 23 | 24 | NAME = 'konfig' 25 | REQUIRES = ['configparser', 'six'] 26 | TESTS_REQUIRES = REQUIRES + [] 27 | META_FILE = read_file('konfig/__init__.py') 28 | LONG_DESCRIPTION = [ read_file(n) for n in ['README.rst', 'CHANGES.txt']] 29 | 30 | setup(name = NAME 31 | , version = find_meta('version') 32 | , description = find_meta('description') 33 | , long_description = '\n\n'.join(LONG_DESCRIPTION) 34 | , url = find_meta('url') 35 | , author = find_meta('author') 36 | , author_email = find_meta('author_email') 37 | , license = find_meta('license') 38 | , keywords = find_meta('keywords') 39 | , packages = find_packages() 40 | , include_package_data = True 41 | , install_requires = REQUIRES 42 | # unfortunately test is not supported by pip (only 'setup.py test') 43 | , tests_require = TESTS_REQUIRES 44 | , test_suite = NAME 45 | , zip_safe = False 46 | , classifiers = [ 47 | "Programming Language :: Python" 48 | , "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)" 49 | , "Development Status :: 5 - Production/Stable" 50 | , ] 51 | , ) 52 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27, py35, lint 3 | 4 | [testenv] 5 | passenv = HOME 6 | usedevelop = True 7 | deps = -r{toxinidir}/requirements.txt 8 | commands = 9 | pytest -v --cov=konfig konfig/tests 10 | 11 | [testenv:lint] 12 | commands = 13 | pylint --rcfile .pylintrc --ignore tests konfig 14 | 15 | --------------------------------------------------------------------------------